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}