Skip to main content

ogeom_algo/fit/
grow.rs

1//! The region of a mesh one surface covers, or the crease one curve runs
2//! along, grown from points clicked on it: [`grow`].
3
4use std::collections::VecDeque;
5
6use ogeom_core::{OgeomError, OgeomResult, Tolerances, ogeom_bail};
7use ogeom_math::{Axis, Circle, Cone, Cylinder, Plane, Point, Sphere, Torus, Vector};
8use ogeom_mesh::Bvh;
9use ogeom_topo::Triangulation;
10
11use super::primitive::{self, Fit, FitFixed, FitOptions, Kind, Model, Side};
12use super::tails;
13use crate::Cutoff;
14use crate::orient::{check_indices, edge_uses, key};
15
16mod meeting;
17mod rim;
18
19/// The surface kinds a region is grown for, and the curve kinds a crease is
20/// traced for.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum FitKind {
24    /// A plane, [`plane`](super::plane).
25    Plane,
26    /// A sphere, [`sphere`](super::sphere).
27    Sphere,
28    /// A cylinder, [`cylinder`](super::cylinder).
29    Cylinder,
30    /// A cone, [`cone`](super::cone).
31    Cone,
32    /// A torus, [`torus`](super::torus).
33    Torus,
34    /// A circle in space, [`circle3`](super::circle3), traced along a
35    /// crease: a bore's or a boss's rim.
36    Circle3,
37    /// A line in space, [`line3`](super::line3), traced along a crease: a
38    /// box's edge.
39    Line3,
40}
41
42impl FitKind {
43    const fn kind(self) -> Kind {
44        match self {
45            Self::Plane => Kind::Plane,
46            Self::Sphere => Kind::Sphere,
47            Self::Cylinder => Kind::Cylinder,
48            Self::Cone => Kind::Cone,
49            Self::Torus => Kind::Torus,
50            Self::Circle3 => Kind::Circle3,
51            Self::Line3 => Kind::Line3,
52        }
53    }
54
55    /// A curve, traced along a crease rather than grown over triangles.
56    const fn curve(self) -> bool {
57        matches!(self, Self::Circle3 | Self::Line3)
58    }
59}
60
61/// How to grow a region.
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct GrowOptions {
64    /// Which points each fit keeps, and how far from the model a vertex may
65    /// lie and its triangle still join: for [`Cutoff::Sigma`] and
66    /// [`Cutoff::Median`] the gate [`grow`] describes, `k σ` over
67    /// many points; for [`Cutoff::Distance`] the distance, widened only by
68    /// the fit's own uncertainty. [`Cutoff::All`] puts no limit on the
69    /// distance, and growth is held by the normals alone.
70    pub cutoff: Cutoff,
71    /// The largest angle, in radians, between a triangle's normal and the
72    /// model's for it to join; above zero and at most π/2. A scan's noise
73    /// turns a triangle's normal by about the noise over the triangle's
74    /// size, which the limit must clear.
75    pub normal_limit: f64,
76    /// Take the region's rim off, ring by ring, while it turns away from
77    /// the model by clearly more than the noise; needs a cutoff with a
78    /// threshold.
79    pub peel: bool,
80    /// The most triangles growth reaches; a seed's first rings are kept
81    /// whole even past it.
82    pub max_triangles: Option<usize>,
83    /// What every fit holds fixed.
84    pub fixed: FitFixed,
85    /// Which side the material is on; where not given, the mesh's normals
86    /// decide.
87    pub side: Option<Side>,
88    /// The angle, in radians, the surface must turn through across an
89    /// edge, between its two triangles' normals, for a curve to be traced
90    /// along it; above zero and below π. As with the normal limit, the
91    /// noise turns an edge by about the noise over the triangles' size,
92    /// which the limit must clear.
93    pub crease_limit: f64,
94}
95
96impl Default for GrowOptions {
97    /// A 3σ cutoff, which keeps 99.7 % of Gaussian noise, and a normal
98    /// limit and a crease limit of π/6, which the noise of a mesh whose
99    /// triangles are ten times the noise across turns past rarely.
100    fn default() -> Self {
101        Self {
102            cutoff: Cutoff::Sigma(3.0),
103            normal_limit: core::f64::consts::FRAC_PI_6,
104            peel: false,
105            max_triangles: None,
106            fixed: FitFixed::default(),
107            side: None,
108            crease_limit: core::f64::consts::FRAC_PI_6,
109        }
110    }
111}
112
113/// A grown region's or a traced crease's fit, of the kind asked for.
114#[derive(Debug, Clone, PartialEq)]
115#[non_exhaustive]
116pub enum AnyFit {
117    /// A plane's fit.
118    Plane(Fit<Plane>),
119    /// A sphere's fit.
120    Sphere(Fit<Sphere>),
121    /// A cylinder's fit.
122    Cylinder(Fit<Cylinder>),
123    /// A cone's fit.
124    Cone(Fit<Cone>),
125    /// A torus's fit.
126    Torus(Fit<Torus>),
127    /// A circle's fit, [`FitKind::Circle3`].
128    Circle(Fit<Circle>),
129    /// A line's fit, [`FitKind::Line3`].
130    Line(Fit<Axis>),
131}
132
133impl AnyFit {
134    /// The kind fitted.
135    #[must_use]
136    pub const fn kind(&self) -> FitKind {
137        match self {
138            Self::Plane(_) => FitKind::Plane,
139            Self::Sphere(_) => FitKind::Sphere,
140            Self::Cylinder(_) => FitKind::Cylinder,
141            Self::Cone(_) => FitKind::Cone,
142            Self::Torus(_) => FitKind::Torus,
143            Self::Circle(_) => FitKind::Circle3,
144            Self::Line(_) => FitKind::Line3,
145        }
146    }
147
148    /// The RMS of the residuals of the points kept.
149    #[must_use]
150    pub fn sigma(&self) -> f64 {
151        self.measured().0
152    }
153
154    /// Each point's signed residual.
155    #[must_use]
156    pub fn residuals(&self) -> &[f64] {
157        self.measured().1
158    }
159
160    /// Whether each point was kept by the last round.
161    #[must_use]
162    pub fn used(&self) -> &[bool] {
163        self.measured().2
164    }
165
166    fn measured(&self) -> (f64, &[f64], &[bool]) {
167        match self {
168            Self::Plane(f) => (f.sigma, &f.residuals, &f.used),
169            Self::Sphere(f) => (f.sigma, &f.residuals, &f.used),
170            Self::Cylinder(f) => (f.sigma, &f.residuals, &f.used),
171            Self::Cone(f) => (f.sigma, &f.residuals, &f.used),
172            Self::Torus(f) => (f.sigma, &f.residuals, &f.used),
173            Self::Circle(f) => (f.sigma, &f.residuals, &f.used),
174            Self::Line(f) => (f.sigma, &f.residuals, &f.used),
175        }
176    }
177
178    fn model(&self) -> Model {
179        match self {
180            Self::Plane(f) => Model::plane(f),
181            Self::Sphere(f) => Model::sphere(f),
182            Self::Cylinder(f) => Model::cylinder(f),
183            Self::Cone(f) => Model::cone(f),
184            Self::Torus(f) => Model::torus(f),
185            Self::Circle(f) => Model::circle3(f),
186            Self::Line(f) => Model::line3(f),
187        }
188    }
189
190    /// The cutoff's threshold over the residuals of the points kept.
191    fn threshold(&self, cutoff: Cutoff) -> f64 {
192        let (_, residuals, used) = self.measured();
193        let kept: Vec<f64> = residuals
194            .iter()
195            .zip(used)
196            .filter_map(|(r, keep)| keep.then_some(*r))
197            .collect();
198        cutoff.threshold(&kept)
199    }
200}
201
202/// How a seed stands against the region's fit.
203#[derive(Debug, Clone, Copy, PartialEq)]
204pub struct SeedReport {
205    /// The triangle nearest the seed.
206    pub triangle: u32,
207    /// Whether that triangle is in the region; for a traced curve, whether
208    /// a crease edge about the seed has both ends on the chain; for a
209    /// [`Crease::Meeting`], whether the triangle lies on the round.
210    pub in_region: bool,
211    /// The seed's residual from the fit.
212    pub offset: f64,
213    /// The angle, in radians, between the triangle's normal and the fit's
214    /// normal at the point of the triangle nearest the seed; for a traced
215    /// curve, the angle the surface turns through across the nearest crease
216    /// edge about the seed on the chain, or the nearest about it where none
217    /// is; for a [`Crease::Meeting`], the angle between its planes.
218    pub angle: f64,
219}
220
221/// A region grown over a mesh, or a crease traced along it, and its fit.
222#[derive(Debug, Clone, PartialEq)]
223#[non_exhaustive]
224pub struct Grown {
225    /// The region's triangles, in increasing order; none for a curve.
226    pub triangles: Vec<u32>,
227    /// The fit over the region's vertices.
228    pub fit: AnyFit,
229    /// The mesh vertices the fit measured, in the order of its residuals:
230    /// those of the region's triangles, in increasing order; for a traced
231    /// curve, the chain's, in their order along it; for a
232    /// [`Crease::Meeting`], the first plane's and then the second's, each
233    /// in increasing order.
234    pub vertices: Vec<u32>,
235    /// Each seed's report, in the seeds' order.
236    pub seeds: Vec<SeedReport>,
237    /// The rings [`GrowOptions::peel`] took off.
238    pub peeled_rings: u32,
239    /// For a curve, how its crease was found; `None` for a surface.
240    pub crease: Option<Crease>,
241}
242
243/// How a [`FitKind::Line3`] or a [`FitKind::Circle3`] found its crease.
244#[derive(Debug, Clone, PartialEq)]
245#[non_exhaustive]
246pub enum Crease {
247    /// Traced along the mesh's crease edges.
248    Traced,
249    /// A line where two planes meet either side of a rounded edge, with no
250    /// crease edge along it: the sharp edge the round stands in for.
251    Meeting(Box<Meeting>),
252}
253
254/// The two planes a [`Crease::Meeting`] line is where they meet.
255#[derive(Debug, Clone, PartialEq)]
256#[non_exhaustive]
257pub struct Meeting {
258    /// Each plane's fit, its normal facing out of the material; the line's
259    /// residuals are theirs, each point's from its own plane.
260    pub planes: [Fit<Plane>; 2],
261    /// Each plane's region's triangles, in increasing order.
262    pub triangles: [Vec<u32>; 2],
263    /// The angle, in radians, between the planes' normals: how far the
264    /// surface turns across the round.
265    pub angle: f64,
266    /// The least and the greatest distance along the line from its
267    /// location, its direction's way, that the round runs between.
268    pub span: (f64, f64),
269}
270
271/// No neighbour across an edge.
272const NONE: u32 = u32::MAX;
273
274/// Grow the region of `mesh` a surface of `kind` covers around `seeds`, and
275/// fit it.
276///
277/// `index` is the hierarchy built over `mesh`. The mesh's triangles are
278/// read as wound counter-clockwise about the material's outward normal,
279/// which tells the fit the material's side where
280/// [`GrowOptions::side`] does not.
281///
282/// # Seeds
283///
284/// Each seed starts at the triangle nearest it. For a plane, its
285/// neighbourhood is that triangle and the rings of triangles round it whose
286/// normals lie within [`GrowOptions::normal_limit`] of its own, until they
287/// hold twice the points the fit needs. For a curved kind it is the region
288/// a plane grows from there: the part of the surface round the seed that
289/// stays within the normal limit of one plane, over which the kind is
290/// determined far better than over a few triangles, fitted from a consensus
291/// start so a neighbouring surface's points that joined it are cut off.
292///
293/// The first seed's neighbourhood is fitted, keeps the triangles its fit
294/// admits, and grows. Each later seed whose neighbourhood the region has not
295/// reached joins where its neighbourhood agrees with the grown fit, and the
296/// region grows again; one that does not agree is left out, and reported
297/// with its offset and angle from the fit.
298///
299/// # Growth
300///
301/// A neighbour across an edge joins where its three vertices lie within the
302/// gate of the model and its normal within the normal limit of the model's
303/// at its centroid. The gate is a prediction interval. A cutoff of `k`
304/// sigmas over a fit of `p` parameters to `n` points measures the noise with
305/// `n - p` degrees of freedom, so the gate is that estimate of the noise
306/// times the Student t as rare as `k` sigmas of a known noise: as wide as a
307/// few points' noise could be, and `k σ` over many. It widens further at a
308/// point by how far the fit's own uncertainty moves the residual there, so a
309/// model a small region determines poorly turns no triangle away for its
310/// own error.
311///
312/// Where every coordinate of the mesh is a single-precision number, no gate
313/// is below what rounding to it could do: a vertex is near within its own
314/// coordinates' rounding along the model's normal, plus the cutoff's sigmas
315/// of the fit's error from the region's rounding, so a noise-free `f32`
316/// mesh grows whole however its rounding fell.
317///
318/// The model is refitted from the last fit whenever the region has doubled
319/// since, and once more whenever no neighbour joins, every neighbour refused
320/// before asked again; growth ends when a whole pass against the latest fit
321/// admits none. So a bore stops at its end faces and a plane at its edges,
322/// however the mesh runs on across them.
323///
324/// # Peeling
325///
326/// A scanner rounds a sharp edge, and the round's first rings lie within
327/// the noise of the face they leave: they join, and pull the fit towards
328/// them. [`GrowOptions::peel`] takes the region's rim off, ring by ring,
329/// while the rim's normals turn away from the model's by clearly more than
330/// the noise turns them.
331///
332/// How far a rim triangle turns, across its rim edges, is to first order a
333/// weighted sum of its vertices' residuals: the gradient of the residuals
334/// interpolated over the triangle, along the in-plane direction out of the
335/// region. Summed over the rim, it is one weighted sum `S = Σ cᵥ rᵥ` of the
336/// rim's vertices' residuals, a vertex shared by several rim triangles
337/// weighing their weights together. Where the residuals are independent
338/// noise of sigma `σ`, `S` is normal with standard deviation `σ √Σ cᵥ²`. A
339/// ring is peeled where `|S|` exceeds the gate times `√Σ cᵥ²`, the gate
340/// being `k σ` for [`Cutoff::Sigma`]`(k)` over many points: a ring of plain
341/// noise is peeled as rarely as a point of plain noise is cut off. The
342/// model is refitted after each ring.
343///
344/// # Curves
345///
346/// A [`FitKind::Circle3`] or a [`FitKind::Line3`] is traced along the
347/// mesh's creases rather than grown over its triangles: a crease edge is
348/// one two triangles share across which the surface turns, between their
349/// normals, by more than [`GrowOptions::crease_limit`]. A seed reaches the
350/// crease edges about the smooth part of the surface it lies on, the
351/// triangles reached from the one nearest it across edges that are not
352/// creases. Of those within reach of the first seed, as many of its
353/// triangle's longest edge as the shortest chain accepted has edges, the
354/// chain starts at the nearest whose chain holds twice the vertices the
355/// kind's fit needs and turns past the limit by more than the noise could
356/// turn it, so a run of edges the noise alone turned past the limit is
357/// passed over; a seed with no such chain about it is refused, by the
358/// nearest chain's fit's own refusal where one refused and as lying on a
359/// smooth part otherwise.
360///
361/// A chain turns past the limit by more than the noise where the angles
362/// its edges turn through, less the limit, sum to more than the noise
363/// could move that sum. Each point moved by noise of the sigma the
364/// chain's fit measures moves the sum, to first order, by its gradient
365/// there; the sum's sigma is the noise's times the root of those
366/// gradients' squares, summed over the points the chain's triangles hold.
367/// The sum must exceed that sigma times the Student t, at the fit's
368/// degrees of freedom, as rare as the cutoff's sigmas over the number of
369/// edges within reach: of all the chains tried, one whose edges turn by
370/// no more than the limit passes as rarely as a point of plain noise lies
371/// that many sigmas out. A short chain measures its noise poorly and must
372/// turn by more. With [`Cutoff::Distance`] or [`Cutoff::All`], which count
373/// no sigmas, every chain within reach passes.
374///
375/// The chain runs from the first seed's edge both ways, a vertex at each
376/// end in turn. Before a fit has a finite gate (with [`Cutoff::All`],
377/// never), it runs on through a vertex only where one crease edge leaves
378/// it. Once one has, it runs on
379/// along the crease edge whose far vertex lies within the gate of the fit,
380/// the gate growth uses, its rounding floor included; where none does, or
381/// where several do and the fit cannot tell which the crease follows, that
382/// end stops. So a rim stops where its vertices leave the circle, and a
383/// box's edge at its corners, where every edge on turns past the line. The
384/// fit is refitted whenever the chain has doubled since the last, and once
385/// more whenever both ends stop, each asked again; tracing ends when a
386/// whole pass against the latest fit adds no vertex, or the chain closes.
387/// A crease that turns at a vertex no other crease meets, before a fit
388/// over few vertices has a gate narrow enough to see it, is followed round
389/// the turn.
390///
391/// A [`FitKind::Circle3`] whose chain stops open runs on once more. A
392/// circle over a short arc is poorly determined: its fit, run on from the
393/// last, can settle in another minimum than one from nothing, a circle of
394/// a hundred times the radius, and its gate is wide enough to admit both
395/// the next vertex of the crease and the far end of a crease edge the
396/// noise turned past the limit off its side. So where the fit lies outside
397/// the confidence region of a fit from nothing, that one is taken, and an
398/// end runs on only through vertices the crease runs on from. The chain
399/// run on is kept where its fit's noise is one the chain it ran on from
400/// could have measured, by the ratio of their variances, and falls back to
401/// where it stopped otherwise. Last, a circle that fits its points no
402/// better than a line by more than the noise could make it, by the sums of
403/// their squared residuals, has a radius they do not determine, and is
404/// refused.
405///
406/// The answer's triangles are empty, and its vertices are the chain's.
407/// Later seeds are reported against the fit, and do not extend the chain.
408/// [`GrowOptions::normal_limit`] and [`GrowOptions::max_triangles`] apply
409/// to regions only, except as below. [`Grown::crease`] is
410/// [`Crease::Traced`].
411///
412/// # Rounded edges
413///
414/// A scan rounds a box's edge, so no edge of it may turn past the crease
415/// limit by more than the noise. Where a [`FitKind::Line3`] finds no chain
416/// about the first seed, the line is sought instead where the planes
417/// either side of the round meet: the sharp edge the round stands in for,
418/// [`Crease::Meeting`].
419///
420/// The round's axis is the way the surface about the seed turns least: the
421/// rings of triangles about the seed's grow until their normals spread one
422/// way across their mean by more than the other, by as much at least as
423/// normals spread evenly over the crease limit. The mesh is cut across
424/// that axis through the seed, and each side's planes are grown, peeled,
425/// from the triangles the cut reaches 1, 2, 4, ... triangles out. A pair
426/// meets across a round where all of these hold:
427///
428/// - neither plane holds the seed's triangle, nor do they share one, and
429///   each keeps twice the points a plane needs;
430/// - each is flat: its sigma squared over the noise its mesh shows inside
431///   it (each inner vertex's offset from its neighbours' mean, taken about
432///   the offsets' mean so a constant bend drops out) is no larger than
433///   the cutoff's sigmas of chance allow, the ratio of two estimates of one
434///   noise; or under [`Cutoff::Distance`], its sigma is within the
435///   distance;
436/// - they turn past the crease limit by more than their own uncertainty
437///   puts on the angle between them, at the cutoff's sigmas;
438/// - the cut reaches each side's plane before the other's, and the line
439///   where they meet lies over the round between where it enters them, by
440///   more than the planes' uncertainty moves it;
441/// - the way across the round along the cut is shorter than each plane is
442///   wide across the line, and they run beside each other along it.
443///
444/// Of those, the pair whose narrower plane is widest for the way across
445/// the round is taken: the faces either side of the round, not a facet of
446/// it nor faces further off. The answer's fit is the line, located in the
447/// middle of where both planes run beside it; its vertices are the first
448/// plane's and then the second's, and its residuals each point's from its
449/// own plane, signed as the plane signs them. [`Meeting`] holds the planes,
450/// their regions, the angle between them and the line's span. A seed is
451/// in the region where its triangle is on neither plane, within the span
452/// and within the round's width of the line; its angle is the planes'.
453/// Where no pair meets across a round, or with [`Cutoff::All`], which has
454/// no threshold to peel by, or with anything held fixed, the seed is
455/// refused as it is for want of a chain.
456///
457/// # Determinism
458///
459/// Everything runs on the calling thread in an order fixed by the seeds and
460/// the triangle indices, and the consensus start draws a fixed sequence:
461/// the same mesh and seeds give the same region and fit bit for bit on any
462/// thread count.
463///
464/// # Errors
465///
466/// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) with no seeds, a
467/// seed that is not finite, a normal limit or a crease limit out of range,
468/// a cutoff whose number is not positive, peeling with [`Cutoff::All`] or
469/// for a curve, a triangle cap of zero, or a triangle naming a vertex the
470/// mesh lacks;
471/// [`OgeomError::Dimension`](ogeom_core::OgeomError::Dimension) where the
472/// hierarchy holds a different number of triangles from the mesh;
473/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) where
474/// the mesh has no triangle to be near a seed, or for a curve where no
475/// crease within reach of the first seed holds twice the vertices the kind
476/// needs and turns past the limit by more than the noise, or none about a
477/// later seed, and, for a line, no two planes meet across a round about
478/// the first, or for a circle where its crease is straight within its
479/// noise; and the kind's
480/// own refusals, as its fit gives them.
481pub fn grow(
482    mesh: &Triangulation,
483    index: &Bvh,
484    seeds: &[Point],
485    kind: FitKind,
486    options: &GrowOptions,
487    tol: Tolerances,
488) -> OgeomResult<Grown> {
489    checked(mesh, index, seeds, kind, options)?;
490    let surface = Surface::of(mesh);
491    if kind.curve() {
492        return rim::traced(&surface, index, seeds, kind, options, tol);
493    }
494    region(&surface, index, seeds, kind, options, tol)
495}
496
497/// The region of `kind` grown over `surface` about the seeds, as [`grow`]
498/// describes, its options already checked.
499fn region(
500    surface: &Surface<'_>,
501    index: &Bvh,
502    seeds: &[Point],
503    kind: FitKind,
504    options: &GrowOptions,
505    tol: Tolerances,
506) -> OgeomResult<Grown> {
507    let least = 2 * primitive::needed(kind.kind(), &options.fixed);
508
509    let mut hits = Vec::with_capacity(seeds.len());
510    for (i, seed) in seeds.iter().enumerate() {
511        let Some(hit) = index.nearest(*seed, f64::INFINITY) else {
512            ogeom_bail!(Construction, "no triangle of the mesh lies near seed {i}");
513        };
514        hits.push(hit);
515    }
516    let mut grower = Grower {
517        surface,
518        kind,
519        options,
520        tol,
521        in_region: vec![false; surface.mesh.triangles.len()],
522        region: Vec::new(),
523        fit: None,
524        gate: None,
525        vertices: Vec::new(),
526    };
527    grower.seeded(&hits)?;
528    let peeled_rings = if options.peel { grower.peel(least)? } else { 0 };
529
530    let Grower {
531        in_region,
532        mut region,
533        fit,
534        gate,
535        vertices,
536        ..
537    } = grower;
538    let (Some(fit), Some(Gate { model, .. })) = (fit, gate) else {
539        ogeom_bail!(Construction, "the seeds' neighbourhoods gave no fit");
540    };
541    let reports = seeds
542        .iter()
543        .zip(&hits)
544        .map(|(seed, hit)| {
545            let t = hit.triangle as usize;
546            SeedReport {
547                triangle: hit.triangle,
548                in_region: in_region[t],
549                offset: model.residual(*seed),
550                angle: angle(&model, surface.normal[t], hit.point),
551            }
552        })
553        .collect();
554    region.sort_unstable();
555    Ok(Grown {
556        triangles: region,
557        fit,
558        vertices,
559        seeds: reports,
560        peeled_rings,
561        crease: None,
562    })
563}
564
565fn checked(
566    mesh: &Triangulation,
567    index: &Bvh,
568    seeds: &[Point],
569    kind: FitKind,
570    options: &GrowOptions,
571) -> OgeomResult<()> {
572    if seeds.is_empty() {
573        ogeom_bail!(Domain, "growing a region needs at least one seed");
574    }
575    if let Some(i) = seeds.iter().position(|p| !p.is_finite()) {
576        ogeom_bail!(Domain, "seed {i} is not finite");
577    }
578    let limit = options.normal_limit;
579    if !(limit > 0.0 && limit <= core::f64::consts::FRAC_PI_2) {
580        ogeom_bail!(
581            Domain,
582            "the normal limit {limit} must lie above 0 and at most pi/2"
583        );
584    }
585    let crease = options.crease_limit;
586    if !(crease > 0.0 && crease < core::f64::consts::PI) {
587        ogeom_bail!(
588            Domain,
589            "the crease limit {crease} must lie above 0 and below pi"
590        );
591    }
592    options.cutoff.check()?;
593    if options.peel && kind.curve() {
594        ogeom_bail!(
595            Domain,
596            "peeling takes off a region's rim, and a curve traced along a crease has none"
597        );
598    }
599    if options.peel && options.cutoff == Cutoff::All {
600        ogeom_bail!(
601            Domain,
602            "peeling measures a rim against the cutoff's threshold, and Cutoff::All has none"
603        );
604    }
605    if options.max_triangles == Some(0) {
606        ogeom_bail!(Domain, "a region capped at no triangles");
607    }
608    check_indices(mesh)?;
609    if index.triangle_count() != mesh.triangles.len() {
610        ogeom_bail!(
611            Dimension,
612            "the hierarchy holds {} triangles and the mesh {}",
613            index.triangle_count(),
614            mesh.triangles.len()
615        );
616    }
617    Ok(())
618}
619
620/// The angle between a triangle's normal and the model's at a point; for
621/// a model without the material's sense, the lesser of the two senses'.
622fn angle(model: &Model, normal: Vector, at: Point) -> f64 {
623    let cos = normal.dot(model.normal(at));
624    let cos = if model.oriented() { cos } else { cos.abs() };
625    cos.clamp(-1.0, 1.0).acos()
626}
627
628/// A mesh read for growing: each triangle's normal, doubled vector area,
629/// centroid and neighbours across its edges, and the triangles about each
630/// vertex.
631struct Surface<'a> {
632    mesh: &'a Triangulation,
633    /// Unit, or zero for a triangle without area.
634    normal: Vec<Vector>,
635    /// Twice the area, along the normal.
636    cross: Vec<Vector>,
637    centroid: Vec<Point>,
638    /// The triangle across each edge `(k, k + 1)` where exactly two use it.
639    across: Vec<[u32; 3]>,
640    fan_start: Vec<usize>,
641    fan: Vec<u32>,
642    /// Whether every coordinate is a single-precision number, as a mesh
643    /// read from an `f32` file's are.
644    single: bool,
645}
646
647impl<'a> Surface<'a> {
648    fn of(mesh: &'a Triangulation) -> Self {
649        let count = mesh.triangles.len();
650        let mut normal = Vec::with_capacity(count);
651        let mut cross = Vec::with_capacity(count);
652        let mut centroid = Vec::with_capacity(count);
653        for t in &mesh.triangles {
654            let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
655            let doubled = (b - a).cross(c - a);
656            let length = doubled.magnitude();
657            if length.is_finite() && length > 0.0 {
658                normal.push(doubled / length);
659                cross.push(doubled);
660            } else {
661                normal.push(Vector::ZERO);
662                cross.push(Vector::ZERO);
663            }
664            centroid.push(a + ((b - a) + (c - a)) / 3.0);
665        }
666        let uses = edge_uses(&mesh.triangles);
667        let across = mesh
668            .triangles
669            .iter()
670            .enumerate()
671            .map(|(t, tri)| {
672                let mut out = [NONE; 3];
673                for (k, slot) in out.iter_mut().enumerate() {
674                    if let Some(u) = uses.get(&key(tri[k], tri[(k + 1) % 3]))
675                        && u.count == 2
676                    {
677                        *slot = if u.first as usize == t {
678                            u.second
679                        } else {
680                            u.first
681                        };
682                    }
683                }
684                out
685            })
686            .collect();
687        let mut fan_start = vec![0_usize; mesh.positions.len() + 1];
688        for tri in &mesh.triangles {
689            for &v in tri {
690                fan_start[v as usize + 1] += 1;
691            }
692        }
693        for v in 0..mesh.positions.len() {
694            fan_start[v + 1] += fan_start[v];
695        }
696        let mut fill = fan_start.clone();
697        let mut fan = vec![0_u32; fan_start[mesh.positions.len()]];
698        for (t, tri) in mesh.triangles.iter().enumerate() {
699            for &v in tri {
700                let slot = &mut fill[v as usize];
701                fan[*slot] = as_index(t);
702                *slot += 1;
703            }
704        }
705        let single = mesh
706            .positions
707            .iter()
708            .all(|p| [p.x, p.y, p.z].iter().all(|&x| is_single(x)));
709        Self {
710            mesh,
711            normal,
712            cross,
713            centroid,
714            across,
715            fan_start,
716            fan,
717            single,
718        }
719    }
720
721    fn corners(&self, t: u32) -> [u32; 3] {
722        self.mesh.triangles[t as usize]
723    }
724
725    fn position(&self, v: u32) -> Point {
726        self.mesh.positions[v as usize]
727    }
728
729    fn fan(&self, v: u32) -> &[u32] {
730        let v = v as usize;
731        &self.fan[self.fan_start[v]..self.fan_start[v + 1]]
732    }
733
734    /// The triangle and the rings of triangles about it, each ring those
735    /// sharing a vertex with the last, at least one ring and until they
736    /// hold `least` vertices or there are no more. Only triangles whose
737    /// normals lie within `cos_limit` of the first's are taken, so a
738    /// triangle beside an edge takes its own side's.
739    fn patch(&self, start: u32, least: usize, cos_limit: f64) -> Vec<u32> {
740        let facing = self.normal[start as usize];
741        let mut triangles = vec![start];
742        let mut vertices: Vec<u32> = self.corners(start).to_vec();
743        vertices.sort_unstable();
744        vertices.dedup();
745        loop {
746            let mut ring: Vec<u32> = vertices
747                .iter()
748                .flat_map(|&v| self.fan(v).iter().copied())
749                .filter(|&t| {
750                    facing == Vector::ZERO || self.normal[t as usize].dot(facing) >= cos_limit
751                })
752                .filter(|t| triangles.binary_search(t).is_err())
753                .collect();
754            ring.sort_unstable();
755            ring.dedup();
756            if ring.is_empty() {
757                break;
758            }
759            for &t in &ring {
760                vertices.extend(self.corners(t));
761            }
762            vertices.sort_unstable();
763            vertices.dedup();
764            triangles.extend(ring);
765            triangles.sort_unstable();
766            if vertices.len() >= least {
767                break;
768            }
769        }
770        triangles
771    }
772
773    /// The vertices of the triangles, in increasing order, each with the
774    /// sum of its triangles' vector areas; a vertex of triangles without
775    /// area alone is left out.
776    fn vertices(&self, triangles: &[u32]) -> (Vec<u32>, Vec<Point>, Vec<Vector>) {
777        let mut sums: Vec<(u32, Vector)> = triangles
778            .iter()
779            .flat_map(|&t| self.corners(t).map(|v| (v, self.cross[t as usize])))
780            .collect();
781        sums.sort_by_key(|(v, _)| *v);
782        let mut vertices = Vec::new();
783        let mut points = Vec::new();
784        let mut normals = Vec::new();
785        let mut i = 0;
786        while i < sums.len() {
787            let v = sums[i].0;
788            let mut sum = Vector::ZERO;
789            while i < sums.len() && sums[i].0 == v {
790                sum += sums[i].1;
791                i += 1;
792            }
793            if sum.magnitude() > 0.0 {
794                vertices.push(v);
795                points.push(self.position(v));
796                normals.push(sum);
797            }
798        }
799        (vertices, points, normals)
800    }
801}
802
803/// Whether `x` is a single-precision number.
804fn is_single(x: f64) -> bool {
805    #[allow(
806        clippy::cast_possible_truncation,
807        reason = "the round trip is the test"
808    )]
809    let narrow = x as f32;
810    f64::from(narrow) == x
811}
812
813/// The most rounding to single precision moves a number of `x`'s size:
814/// half the gap from there to the next single-precision number up, which
815/// is the wider gap about it.
816fn half_ulp(x: f64) -> f64 {
817    #[allow(
818        clippy::cast_possible_truncation,
819        reason = "the size of a single-precision coordinate, or past the largest"
820    )]
821    let size = x.abs() as f32;
822    let up = f64::from(size.next_up());
823    let gap = if up.is_finite() {
824        up - f64::from(size)
825    } else {
826        f64::from(size) - f64::from(size.next_down())
827    };
828    0.5 * gap
829}
830
831fn as_index(t: usize) -> u32 {
832    #[allow(
833        clippy::cast_possible_truncation,
834        reason = "the hierarchy holds the mesh's triangles, which it refuses past u32"
835    )]
836    let t = t as u32;
837    t
838}
839
840/// What decides whether a triangle joins: the model and the distance and
841/// angle it allows.
842#[derive(Debug, Clone, Copy)]
843struct Gate {
844    model: Model,
845    threshold: f64,
846    cos_limit: f64,
847    /// For a single-precision mesh, the most rounding moved a coordinate
848    /// of the region's size; zero otherwise.
849    quantum: f64,
850    /// How many sigmas of the noise the cutoff keeps.
851    sigmas: f64,
852    /// The noise's sigma along each direction a residual is measured in,
853    /// as the fit's residuals estimate it, and the estimate's degrees of
854    /// freedom; zero for a cutoff without sigmas.
855    noise: f64,
856    dof: usize,
857}
858
859impl Gate {
860    /// The gate of a fit to `points`.
861    ///
862    /// A cutoff of `k` sigmas (or a median's worth of them) measured over
863    /// a fit of `p` parameters to `n` points, each missing the model in `c`
864    /// directions (two for a curve in space, one for a surface), is a noise
865    /// estimated with `c n - p` degrees of freedom: the threshold is the
866    /// sigma that estimate gives, `σ √(c n / (c n - p))`, times the Student
867    /// t at which a residual is as rare as `k` sigmas of a known noise. A few points then admit
868    /// as much as their noise could, and many admit `k σ`. No threshold is
869    /// below the confusion distance, within which points are one.
870    ///
871    /// On a single-precision mesh, no threshold is below the rounding's
872    /// either. Rounding moves each coordinate by at most `q`, half the gap
873    /// between single-precision numbers at the region's size, so it moves a
874    /// residual along a unit normal by a sigma of at most `q`: the
875    /// threshold is at least as many of those sigmas as the cutoff keeps of
876    /// the fit's own.
877    fn of(
878        fit: &AnyFit,
879        points: &[Point],
880        single: bool,
881        options: &GrowOptions,
882        tol: Tolerances,
883    ) -> Self {
884        let model = fit
885            .model()
886            .measured_over(points, fit.used(), &options.fixed);
887        let (mut threshold, mut quantum, mut sigmas) = (f64::INFINITY, 0.0, 0.0);
888        let (mut noise, mut dof) = (0.0, 0);
889        match options.cutoff {
890            Cutoff::All => {}
891            Cutoff::Distance(d) => threshold = d,
892            cutoff @ (Cutoff::Sigma(k) | Cutoff::Median(k)) => {
893                let kept = model.components() * fit.used().iter().filter(|u| **u).count();
894                dof = kept.saturating_sub(model.parameters());
895                let base = fit.threshold(cutoff);
896                let rms = fit.sigma();
897                sigmas = if rms > 0.0 { base / rms } else { k };
898                threshold = if dof == 0 {
899                    f64::INFINITY
900                } else if rms > 0.0 {
901                    #[allow(
902                        clippy::cast_precision_loss,
903                        reason = "point counts are far below 2^52"
904                    )]
905                    let unbiased = rms * (kept as f64 / dof as f64).sqrt();
906                    #[allow(clippy::cast_precision_loss, reason = "one, two or three")]
907                    let along = model.components() as f64;
908                    noise = unbiased / along.sqrt();
909                    tails::t_at_normal(sigmas, dof) * unbiased
910                } else {
911                    0.0
912                };
913                if single {
914                    let size = points
915                        .iter()
916                        .flat_map(|p| [p.x, p.y, p.z])
917                        .fold(0.0, |m: f64, x| m.max(x.abs()));
918                    quantum = half_ulp(size);
919                    threshold = threshold.max(sigmas * quantum);
920                }
921            }
922        }
923        Self {
924            model,
925            threshold: threshold.max(tol.confusion()),
926            cos_limit: options.normal_limit.cos(),
927            quantum,
928            sigmas,
929            noise,
930            dof,
931        }
932    }
933
934    /// Whether a point lies within the threshold, widened by how far the
935    /// fit's own uncertainty moves its residual there: the prediction
936    /// interval of a point the fit did not see, so a model a small region
937    /// determines poorly turns no triangle away for its own error.
938    ///
939    /// On a single-precision mesh, a point is near at least where its
940    /// residual is within what rounding alone could have moved it: its own
941    /// coordinates' rounding, along the normal, at most (for a curve, the
942    /// rounding's half diagonal), and the fit's
943    /// error from the region's rounding, as many sigmas of it as the
944    /// cutoff keeps.
945    fn near(&self, p: Point) -> bool {
946        let r = self.model.residual(p);
947        let leverage = self.model.leverage(p);
948        let mut bound = self.threshold * (1.0 + leverage).sqrt();
949        if self.quantum > 0.0 {
950            let half = Vector::new(half_ulp(p.x), half_ulp(p.y), half_ulp(p.z));
951            let own = self.model.moved_by(p, half);
952            bound = bound.max(own + self.sigmas * self.quantum * leverage.sqrt());
953        }
954        r.abs() <= bound
955    }
956
957    fn admits(&self, surface: &Surface<'_>, t: u32) -> bool {
958        let i = t as usize;
959        if surface.cross[i] == Vector::ZERO {
960            return false;
961        }
962        let near = surface
963            .corners(t)
964            .iter()
965            .all(|&v| self.near(surface.position(v)));
966        near && {
967            let cos = surface.normal[i].dot(self.model.normal(surface.centroid[i]));
968            let cos = if self.model.oriented() {
969                cos
970            } else {
971                cos.abs()
972            };
973            cos >= self.cos_limit
974        }
975    }
976}
977
978/// A region as it grows, and its latest fit.
979struct Grower<'s, 'a> {
980    surface: &'s Surface<'a>,
981    kind: FitKind,
982    options: &'s GrowOptions,
983    tol: Tolerances,
984    in_region: Vec<bool>,
985    /// The region's triangles, in the order they joined.
986    region: Vec<u32>,
987    fit: Option<AnyFit>,
988    gate: Option<Gate>,
989    vertices: Vec<u32>,
990}
991
992impl Grower<'_, '_> {
993    /// Fit the region, from the last fit where there is one.
994    fn refit(&mut self) -> OgeomResult<Gate> {
995        let (vertices, points, normals) = self.surface.vertices(&self.region);
996        let fit = fitted(
997            self.kind,
998            &points,
999            Some(&normals),
1000            self.options,
1001            self.fit.as_ref(),
1002            self.tol,
1003        )?;
1004        self.vertices = vertices;
1005        let gate = Gate::of(&fit, &points, self.surface.single, self.options, self.tol);
1006        self.gate = Some(gate);
1007        self.fit = Some(fit);
1008        Ok(gate)
1009    }
1010
1011    fn set_region(&mut self, triangles: Vec<u32>) {
1012        for &t in &self.region {
1013            self.in_region[t as usize] = false;
1014        }
1015        for &t in &triangles {
1016            self.in_region[t as usize] = true;
1017        }
1018        self.region = triangles;
1019    }
1020
1021    /// A seed's neighbourhood: for a plane, the rings about the hit
1022    /// triangle; for a curved kind, the region a plane grows from them
1023    /// with these options.
1024    fn neighbourhood(&self, hit: &ogeom_mesh::Hit) -> OgeomResult<Vec<u32>> {
1025        let least = 2 * primitive::needed(Kind::Plane, &FitFixed::default());
1026        let start = self
1027            .surface
1028            .patch(hit.triangle, least, self.options.normal_limit.cos());
1029        if self.kind == FitKind::Plane {
1030            return Ok(start);
1031        }
1032        let options = GrowOptions {
1033            peel: false,
1034            fixed: FitFixed::default(),
1035            side: None,
1036            ..*self.options
1037        };
1038        let mut plane = Grower {
1039            surface: self.surface,
1040            kind: FitKind::Plane,
1041            options: &options,
1042            tol: self.tol,
1043            in_region: vec![false; self.in_region.len()],
1044            region: Vec::new(),
1045            fit: None,
1046            gate: None,
1047            vertices: Vec::new(),
1048        };
1049        plane.set_region(start);
1050        plane.refit()?;
1051        plane.spread()?;
1052        plane.region.sort_unstable();
1053        Ok(plane.region)
1054    }
1055
1056    /// Grow from the seeds in their order. The first neighbourhoods are
1057    /// fitted together until they determine a fit, which keeps the
1058    /// triangles it admits and grows. Each later neighbourhood the region
1059    /// has not reached joins where it agrees with the grown fit, with the
1060    /// triangles that fit admits, and the region grows again; one that does
1061    /// not agree is left out.
1062    fn seeded(&mut self, hits: &[ogeom_mesh::Hit]) -> OgeomResult<()> {
1063        let patches = hits
1064            .iter()
1065            .map(|h| self.neighbourhood(h))
1066            .collect::<OgeomResult<Vec<_>>>()?;
1067        let mut first: Vec<u32> = Vec::new();
1068        for (i, patch) in patches.iter().enumerate() {
1069            let Some(gate) = self.gate else {
1070                first.extend(patch);
1071                first.sort_unstable();
1072                first.dedup();
1073                self.set_region(first.clone());
1074                match self.refit() {
1075                    Ok(gate) => {
1076                        let kept: Vec<u32> = first
1077                            .iter()
1078                            .copied()
1079                            .filter(|&t| gate.admits(self.surface, t))
1080                            .collect();
1081                        if kept.len() != first.len() {
1082                            self.set_region(kept);
1083                            self.refit()?;
1084                        }
1085                        self.spread()?;
1086                    }
1087                    Err(OgeomError::Construction(_)) if i + 1 < patches.len() => {}
1088                    Err(e) => return Err(e),
1089                }
1090                continue;
1091            };
1092            if patch.iter().all(|&t| self.in_region[t as usize])
1093                || self.disagreement(&gate, patch) > 1.0
1094            {
1095                continue;
1096            }
1097            let joining: Vec<u32> = patch
1098                .iter()
1099                .copied()
1100                .filter(|&t| !self.in_region[t as usize] && gate.admits(self.surface, t))
1101                .collect();
1102            if joining.is_empty() {
1103                continue;
1104            }
1105            for t in joining {
1106                self.in_region[t as usize] = true;
1107                self.region.push(t);
1108            }
1109            self.refit()?;
1110            self.spread()?;
1111        }
1112        Ok(())
1113    }
1114
1115    /// How far a neighbourhood is from agreeing with the fit, as the larger
1116    /// of its vertices' RMS residual over the gate, each residual shrunk by
1117    /// the widening the gate gives it, and its mean normal's angle over the
1118    /// limit: it agrees at one or less.
1119    fn disagreement(&self, gate: &Gate, patch: &[u32]) -> f64 {
1120        let surface = self.surface;
1121        let (_, points, _) = surface.vertices(patch);
1122        let squares: f64 = points
1123            .iter()
1124            .map(|p| gate.model.residual(*p).powi(2) / (1.0 + gate.model.leverage(*p)))
1125            .sum();
1126        #[allow(
1127            clippy::cast_precision_loss,
1128            reason = "point counts are far below 2^52"
1129        )]
1130        let rms = (squares / points.len().max(1) as f64).sqrt();
1131        let (mut cos, mut area) = (0.0, 0.0);
1132        for &t in patch {
1133            let i = t as usize;
1134            let a = surface.cross[i].magnitude();
1135            let c = surface.normal[i].dot(gate.model.normal(surface.centroid[i]));
1136            cos += a * if gate.model.oriented() { c } else { c.abs() };
1137            area += a;
1138        }
1139        let mean_angle = if area > 0.0 {
1140            (cos / area).clamp(-1.0, 1.0).acos()
1141        } else {
1142            core::f64::consts::PI
1143        };
1144        ratio(rms, gate.threshold).max(ratio(mean_angle, self.options.normal_limit))
1145    }
1146
1147    fn capped(&self) -> bool {
1148        self.options
1149            .max_triangles
1150            .is_some_and(|most| self.region.len() >= most)
1151    }
1152
1153    /// Grow across edges until a whole pass against the latest fit admits
1154    /// no triangle.
1155    fn spread(&mut self) -> OgeomResult<()> {
1156        let surface = self.surface;
1157        let count = self.in_region.len();
1158        let mut queued = vec![false; count];
1159        let mut refused = vec![false; count];
1160        let mut waiting: Vec<u32> = Vec::new();
1161        let mut queue: VecDeque<u32> = VecDeque::new();
1162        for &t in &self.region {
1163            for &n in &surface.across[t as usize] {
1164                if n != NONE && !self.in_region[n as usize] && !queued[n as usize] {
1165                    queued[n as usize] = true;
1166                    queue.push_back(n);
1167                }
1168            }
1169        }
1170        let Some(mut gate) = self.gate else {
1171            ogeom_bail!(Construction, "the seeds' neighbourhoods gave no fit");
1172        };
1173        let mut fitted_at = self.region.len();
1174        loop {
1175            while let Some(t) = queue.pop_front() {
1176                queued[t as usize] = false;
1177                if self.capped() {
1178                    queue.clear();
1179                    break;
1180                }
1181                if self.in_region[t as usize] {
1182                    continue;
1183                }
1184                if !gate.admits(surface, t) {
1185                    if !refused[t as usize] {
1186                        refused[t as usize] = true;
1187                        waiting.push(t);
1188                    }
1189                    continue;
1190                }
1191                self.in_region[t as usize] = true;
1192                self.region.push(t);
1193                for &n in &surface.across[t as usize] {
1194                    if n != NONE && !self.in_region[n as usize] && !queued[n as usize] {
1195                        queued[n as usize] = true;
1196                        queue.push_back(n);
1197                    }
1198                }
1199                if self.region.len() >= 2 * fitted_at {
1200                    gate = self.refit()?;
1201                    fitted_at = self.region.len();
1202                    requeue(&mut waiting, &mut refused, &mut queued, &mut queue);
1203                }
1204            }
1205            if self.region.len() == fitted_at {
1206                break;
1207            }
1208            gate = self.refit()?;
1209            fitted_at = self.region.len();
1210            if self.capped() {
1211                break;
1212            }
1213            requeue(&mut waiting, &mut refused, &mut queued, &mut queue);
1214            if queue.is_empty() {
1215                break;
1216            }
1217        }
1218        Ok(())
1219    }
1220
1221    /// Take the rim off, ring by ring, while it turns away from the model
1222    /// by clearly more than the noise, keeping at least `least` vertices.
1223    fn peel(&mut self, least: usize) -> OgeomResult<u32> {
1224        let surface = self.surface;
1225        let mut rings = 0;
1226        let mut weight = vec![0.0_f64; surface.mesh.positions.len()];
1227        let mut marked = vec![false; surface.mesh.positions.len()];
1228        loop {
1229            let Some(Gate {
1230                model, threshold, ..
1231            }) = self.gate
1232            else {
1233                ogeom_bail!(Construction, "the seeds' neighbourhoods gave no fit");
1234            };
1235            self.region.sort_unstable();
1236            let mut inner = Vec::new();
1237            let mut touched: Vec<u32> = Vec::new();
1238            for &t in &self.region {
1239                let i = t as usize;
1240                let corners = surface.corners(t);
1241                let p = corners.map(|v| surface.position(v));
1242                let n = surface.normal[i];
1243                let mut out = Vector::ZERO;
1244                let mut on_rim = false;
1245                for k in 0..3 {
1246                    let other = surface.across[i][k];
1247                    if other != NONE && self.in_region[other as usize] {
1248                        continue;
1249                    }
1250                    on_rim = true;
1251                    let (a, b, c) = (p[k], p[(k + 1) % 3], p[(k + 2) % 3]);
1252                    let side = (b - a).cross(n);
1253                    out += if side.dot(c - a) > 0.0 { -side } else { side };
1254                }
1255                if !on_rim {
1256                    inner.push(t);
1257                    continue;
1258                }
1259                let length = out.magnitude();
1260                if length.is_nan() || length <= 0.0 {
1261                    continue;
1262                }
1263                let out = out / length;
1264                let doubled = surface.cross[i].magnitude();
1265                for k in 0..3 {
1266                    // The gradient of the barycentric weight of corner k.
1267                    let opposite = p[(k + 2) % 3] - p[(k + 1) % 3];
1268                    let gradient = n.cross(opposite) / doubled;
1269                    let v = corners[k];
1270                    if !marked[v as usize] {
1271                        marked[v as usize] = true;
1272                        touched.push(v);
1273                    }
1274                    weight[v as usize] += gradient.dot(out);
1275                }
1276            }
1277            let (mut sum, mut squares) = (0.0, 0.0);
1278            for &v in &touched {
1279                let c = weight[v as usize];
1280                sum += c * model.residual(surface.position(v));
1281                squares += c * c;
1282                weight[v as usize] = 0.0;
1283                marked[v as usize] = false;
1284            }
1285            let clear = sum.abs() > threshold * squares.sqrt();
1286            if !clear || inner.is_empty() || surface.vertices(&inner).0.len() < least {
1287                break;
1288            }
1289            self.set_region(inner);
1290            self.refit()?;
1291            rings += 1;
1292        }
1293        Ok(rings)
1294    }
1295}
1296
1297fn requeue(
1298    waiting: &mut Vec<u32>,
1299    refused: &mut [bool],
1300    queued: &mut [bool],
1301    queue: &mut VecDeque<u32>,
1302) {
1303    for t in waiting.drain(..) {
1304        refused[t as usize] = false;
1305        if !queued[t as usize] {
1306            queued[t as usize] = true;
1307            queue.push_back(t);
1308        }
1309    }
1310}
1311
1312/// `a` over `b`: zero for an `a` of zero or an infinite `b`, and
1313/// infinite for a positive `a` over a zero `b`.
1314fn ratio(a: f64, b: f64) -> f64 {
1315    if a <= 0.0 || b.is_infinite() {
1316        0.0
1317    } else if b > 0.0 {
1318        a / b
1319    } else {
1320        f64::INFINITY
1321    }
1322}
1323
1324/// The kind's fit to the points, with their normals, from the last fit's
1325/// shape where there is one.
1326fn fitted(
1327    kind: FitKind,
1328    points: &[Point],
1329    normals: Option<&[Vector]>,
1330    options: &GrowOptions,
1331    last: Option<&AnyFit>,
1332    tol: Tolerances,
1333) -> OgeomResult<AnyFit> {
1334    fn with<'n, S>(
1335        options: &GrowOptions,
1336        normals: Option<&'n [Vector]>,
1337        guess: Option<S>,
1338        robust: bool,
1339    ) -> FitOptions<'n, S> {
1340        FitOptions {
1341            normals,
1342            cutoff: options.cutoff,
1343            robust_start: robust && guess.is_none() && options.cutoff != Cutoff::All,
1344            fixed: options.fixed,
1345            guess,
1346            side: options.side,
1347            radius_range: None,
1348        }
1349    }
1350    // A consensus start for a curved kind, whose start from a
1351    // neighbourhood a plane grew may hold another surface's points; a
1352    // plane's and a line's closed forms start from nothing.
1353    let robust = !matches!(kind, FitKind::Plane | FitKind::Line3);
1354    Ok(match kind {
1355        FitKind::Plane => {
1356            let guess = match last {
1357                Some(AnyFit::Plane(f)) => Some(f.shape),
1358                _ => None,
1359            };
1360            AnyFit::Plane(primitive::plane(
1361                points,
1362                &with(options, normals, guess, robust),
1363                tol,
1364            )?)
1365        }
1366        FitKind::Sphere => {
1367            let guess = match last {
1368                Some(AnyFit::Sphere(f)) => Some(f.shape),
1369                _ => None,
1370            };
1371            AnyFit::Sphere(primitive::sphere(
1372                points,
1373                &with(options, normals, guess, robust),
1374                tol,
1375            )?)
1376        }
1377        FitKind::Cylinder => {
1378            let guess = match last {
1379                Some(AnyFit::Cylinder(f)) => Some(f.shape),
1380                _ => None,
1381            };
1382            AnyFit::Cylinder(primitive::cylinder(
1383                points,
1384                &with(options, normals, guess, robust),
1385                tol,
1386            )?)
1387        }
1388        FitKind::Cone => {
1389            let guess = match last {
1390                Some(AnyFit::Cone(f)) => Some(f.shape),
1391                _ => None,
1392            };
1393            AnyFit::Cone(primitive::cone(
1394                points,
1395                &with(options, normals, guess, robust),
1396                tol,
1397            )?)
1398        }
1399        FitKind::Torus => {
1400            let guess = match last {
1401                Some(AnyFit::Torus(f)) => Some(f.shape),
1402                _ => None,
1403            };
1404            AnyFit::Torus(primitive::torus(
1405                points,
1406                &with(options, normals, guess, robust),
1407                tol,
1408            )?)
1409        }
1410        FitKind::Circle3 => {
1411            let guess = match last {
1412                Some(AnyFit::Circle(f)) => Some(f.shape),
1413                _ => None,
1414            };
1415            AnyFit::Circle(primitive::circle3(
1416                points,
1417                &with(options, normals, guess, robust),
1418                tol,
1419            )?)
1420        }
1421        FitKind::Line3 => AnyFit::Line(primitive::line3(
1422            points,
1423            &with(options, normals, None, robust),
1424            tol,
1425        )?),
1426    })
1427}