Skip to main content

brep_ransac/
recognize.rs

1use crate::fit::{
2    evaluate_surface, evaluate_surface_from_vertices, fit_surface_from_vertices_with_path,
3    fit_surface_with_path, selection_scale, vertex_selection_scale,
4};
5use crate::numerical::recognition as numerical;
6use crate::{
7    AnalyticSurface, AnalyzedMesh, ConstraintMask, FitPath, Mesh, MeshAnalysisOptions,
8    MetadataTrust, RecognitionError, RecognitionOptions, RecognitionResult, SurfaceFitResult,
9    SurfaceHint, SurfaceRegion, SurfaceType, UnresolvedRegionDiagnostic,
10};
11use std::cmp::Ordering;
12use std::collections::{BTreeSet, VecDeque};
13use web_time::Instant;
14
15const TYPES: [SurfaceType; 5] = [
16    SurfaceType::Plane,
17    SurfaceType::Sphere,
18    SurfaceType::Cylinder,
19    SurfaceType::Cone,
20    SurfaceType::Torus,
21];
22
23const MODEL_COMPLEXITY_PENALTY_UNIT: f64 = numerical::MODEL_COMPLEXITY_PENALTY_UNIT;
24// BREP private tests: 74babe4b412f9674
25// The probe supplies spatial/normal diversity without increasing the number
26// of triangles actually passed to a primitive fitter.
27const HYPOTHESIS_DIVERSITY_PROBE_TRIANGLES: usize = 128;
28
29#[derive(Clone, Copy, Debug)]
30struct ModelRank {
31    numerically_exact: bool,
32    score: f64,
33}
34
35fn compare_model_rank(
36    left: ModelRank,
37    left_kind: SurfaceType,
38    right: ModelRank,
39    right_kind: SurfaceType,
40) -> Ordering {
41    right
42        .numerically_exact
43        .cmp(&left.numerically_exact)
44        .then_with(|| left.score.total_cmp(&right.score))
45        .then(left_kind.cmp(&right_kind))
46}
47
48/// Reconstruct one selected triangle region using an explicit metadata hint.
49///
50/// The selection and options are validated before fitting. Exact candidates
51/// are reused unchanged only after their position, normal, and support gates
52/// pass; weaker hints select the corresponding refinement path.
53pub fn reconstruct_surface(
54    mesh: &Mesh,
55    triangle_indices: &[usize],
56    hint: &SurfaceHint,
57    options: &RecognitionOptions,
58) -> Result<SurfaceFitResult, RecognitionError> {
59    options.validate()?;
60    let analyzed = mesh.analyze(&MeshAnalysisOptions {
61        feature_angle: options.feature_angle,
62        ..Default::default()
63    })?;
64    analyzed.validate_selection(triangle_indices)?;
65    reconstruct_analyzed(&analyzed, triangle_indices, hint, options)
66}
67
68/// Reconstruct one surface from an explicit subset of mesh vertices.
69///
70/// Unlike [`reconstruct_surface`], this entry point fits exactly the requested
71/// vertices; it does not expand them to their incident triangles. Per-vertex
72/// normals are used when supplied, otherwise area-weighted incident triangle
73/// normals are derived. `minimum_support` and `minimum_support_area` continue
74/// to gate the usable incident-triangle support represented by the vertices.
75pub fn reconstruct_surface_from_vertices(
76    mesh: &Mesh,
77    vertex_indices: &[usize],
78    hint: &SurfaceHint,
79    options: &RecognitionOptions,
80) -> Result<SurfaceFitResult, RecognitionError> {
81    options.validate()?;
82    let analyzed = mesh.analyze(&MeshAnalysisOptions {
83        feature_angle: options.feature_angle,
84        ..Default::default()
85    })?;
86    analyzed.validate_vertex_selection(vertex_indices)?;
87    reconstruct_selected(
88        &analyzed,
89        Selection::Vertices(vertex_indices),
90        hint,
91        options,
92    )
93}
94
95#[derive(Clone, Copy)]
96enum Selection<'a> {
97    Triangles(&'a [usize]),
98    Vertices(&'a [usize]),
99}
100
101impl Selection<'_> {
102    fn scale(self, mesh: &AnalyzedMesh) -> f64 {
103        match self {
104            Self::Triangles(ids) => selection_scale(mesh, ids),
105            Self::Vertices(ids) => vertex_selection_scale(mesh, ids),
106        }
107    }
108
109    fn fit(
110        self,
111        mesh: &AnalyzedMesh,
112        kind: SurfaceType,
113        initial: Option<AnalyticSurface>,
114        fixed: ConstraintMask,
115        options: &RecognitionOptions,
116        path: FitPath,
117    ) -> Result<SurfaceFitResult, RecognitionError> {
118        match self {
119            Self::Triangles(ids) => {
120                fit_surface_with_path(mesh, ids, kind, initial, fixed, options, path)
121            }
122            Self::Vertices(ids) => {
123                fit_surface_from_vertices_with_path(mesh, ids, kind, initial, fixed, options, path)
124            }
125        }
126    }
127
128    fn evaluate(
129        self,
130        mesh: &AnalyzedMesh,
131        surface: AnalyticSurface,
132        options: &RecognitionOptions,
133        path: FitPath,
134    ) -> Result<SurfaceFitResult, RecognitionError> {
135        match self {
136            Self::Triangles(ids) => evaluate_surface(mesh, ids, surface, options, path),
137            Self::Vertices(ids) => {
138                evaluate_surface_from_vertices(mesh, ids, surface, options, path)
139            }
140        }
141    }
142
143    fn accepted(self, model: &SurfaceFitResult, options: &RecognitionOptions, scale: f64) -> bool {
144        accepted_impl(
145            model,
146            options,
147            scale,
148            matches!(self, Self::Vertices(_))
149                || matches!(options.sampling, crate::SamplingMode::Vertices),
150        )
151    }
152}
153
154/// Discover and reconstruct analytic regions in an entire mesh.
155///
156/// This convenience entry point returns only accepted regions. Use
157/// [`recognize_surfaces_with_unresolved`] when the unassigned triangle
158/// partition is also required.
159pub fn recognize_surfaces(
160    mesh: &Mesh,
161    options: &RecognitionOptions,
162) -> Result<Vec<SurfaceRegion>, RecognitionError> {
163    Ok(recognize_surfaces_with_unresolved(mesh, options)?.regions)
164}
165
166/// Discover analytic regions and retain triangles that no model accepted.
167///
168/// Metadata sidecars are evaluated before generic connected-component
169/// extraction. Returned regions and unresolved triangle IDs are ordered
170/// deterministically for a fixed mesh, options, and seed.
171pub fn recognize_surfaces_with_unresolved(
172    mesh: &Mesh,
173    options: &RecognitionOptions,
174) -> Result<RecognitionResult, RecognitionError> {
175    options.validate()?;
176    let analyzed = mesh.analyze(&MeshAnalysisOptions {
177        feature_angle: options.feature_angle,
178        ..Default::default()
179    })?;
180    let mut assigned = vec![false; analyzed.triangles.len()];
181    let mut regions = Vec::new();
182    let mut metadata_failures = Vec::new();
183    // Metadata regions are deliberately inspected before generic extraction.
184    for metadata in &analyzed.source_metadata {
185        let ids: Vec<_> = metadata
186            .triangle_indices
187            .iter()
188            .copied()
189            .filter(|&i| !assigned[i] && analyzed.triangles[i].area > 0.0)
190            .collect();
191        if ids.is_empty() {
192            continue;
193        }
194        let mut metadata_options = options.clone();
195        if let Some(source_tolerance) = metadata.source_tolerance {
196            metadata_options.distance_tolerance =
197                metadata_options.distance_tolerance.max(source_tolerance);
198        }
199        match reconstruct_analyzed(&analyzed, &ids, &metadata.hint, &metadata_options) {
200            Ok(mut fit)
201                if metadata
202                    .orientation
203                    .is_none_or(|orientation| orientation == fit.orientation) =>
204            {
205                if let Some(orientation) = metadata.orientation {
206                    fit.orientation = orientation;
207                }
208                if metadata.source_tolerance.is_some() || metadata.orientation.is_some() {
209                    fit.diagnostics.reason.push_str(
210                        "; source orientation and tolerance metadata validated when supplied",
211                    );
212                }
213                for &i in &ids {
214                    assigned[i] = true;
215                }
216                regions.push(to_region(fit, ids));
217            }
218            Ok(fit) => metadata_failures.push((
219                metadata.clone(),
220                ids,
221                format!(
222                    "supplied metadata orientation {:?} disagrees with reconstructed orientation {}",
223                    metadata.orientation, fit.orientation
224                ),
225            )),
226            Err(error) => metadata_failures.push((metadata.clone(), ids, error.to_string())),
227        }
228    }
229    let remaining: Vec<_> = analyzed
230        .all_non_degenerate()
231        .into_iter()
232        .filter(|&i| !assigned[i])
233        .collect();
234    let components = if options.discover_regions {
235        analyzed.connected_components(&remaining, options.respect_features)
236    } else {
237        vec![remaining]
238    };
239    for component in components {
240        extract_component(&analyzed, &component, options, &mut regions);
241    }
242    if options.allow_disconnected_same_surface {
243        merge_disconnected_regions(&analyzed, options, &mut regions);
244    }
245    regions.sort_by_key(|region| {
246        region
247            .triangle_indices
248            .first()
249            .copied()
250            .unwrap_or(usize::MAX)
251    });
252    let mut covered = vec![false; analyzed.triangles.len()];
253    for region in &regions {
254        for &id in &region.triangle_indices {
255            covered[id] = true;
256        }
257    }
258    let unresolved_triangles = analyzed
259        .all_non_degenerate()
260        .into_iter()
261        .filter(|&id| !covered[id])
262        .collect::<Vec<_>>();
263    let unresolved_set: BTreeSet<_> = unresolved_triangles.iter().copied().collect();
264    let mut unresolved_diagnostics = metadata_failures
265        .into_iter()
266        .filter_map(|(metadata, ids, metadata_error)| {
267            let mut triangle_indices: Vec<_> = ids
268                .into_iter()
269                .filter(|id| unresolved_set.contains(id))
270                .collect();
271            triangle_indices.sort_unstable();
272            triangle_indices.dedup();
273            (!triangle_indices.is_empty()).then(|| UnresolvedRegionDiagnostic {
274                triangle_indices,
275                source_face_id: metadata.source_face_id,
276                source_face_name: metadata.source_face_name,
277                source_surface_id: metadata.source_surface_id,
278                reason: format!(
279                    "source metadata validation or reconstruction failed ({metadata_error}); generic analytic fallback also left this subset unresolved"
280                ),
281            })
282        })
283        .collect::<Vec<_>>();
284    unresolved_diagnostics.sort_by(|left, right| {
285        left.triangle_indices
286            .cmp(&right.triangle_indices)
287            .then_with(|| left.source_face_id.cmp(&right.source_face_id))
288            .then_with(|| left.source_face_name.cmp(&right.source_face_name))
289            .then_with(|| left.source_surface_id.cmp(&right.source_surface_id))
290            .then_with(|| left.reason.cmp(&right.reason))
291    });
292    Ok(RecognitionResult {
293        regions,
294        unresolved_triangles,
295        unresolved_diagnostics,
296    })
297}
298
299/// Reconstruct a selected triangle region from an already analyzed mesh.
300///
301/// Integration crates can use this seam to avoid repeating mesh analysis when
302/// validating many independently owned source faces.
303#[doc(hidden)]
304pub fn reconstruct_analyzed(
305    mesh: &AnalyzedMesh,
306    ids: &[usize],
307    hint: &SurfaceHint,
308    options: &RecognitionOptions,
309) -> Result<SurfaceFitResult, RecognitionError> {
310    // This crate-private entry point is used by STEP validation to reuse mesh
311    // analysis across recognition modes. It must preserve every public-entry
312    // invariant rather than assuming its caller already validated options.
313    options.validate()?;
314    mesh.validate_selection(ids)?;
315    reconstruct_selected(mesh, Selection::Triangles(ids), hint, options)
316}
317
318fn reconstruct_selected(
319    mesh: &AnalyzedMesh,
320    selection: Selection<'_>,
321    hint: &SurfaceHint,
322    options: &RecognitionOptions,
323) -> Result<SurfaceFitResult, RecognitionError> {
324    let scale = selection.scale(mesh);
325    let metadata_started = options.collect_phase_timings.then(Instant::now);
326    let metadata_trust = match hint {
327        SurfaceHint::Unknown => MetadataTrust::Unknown,
328        SurfaceHint::KnownType { .. } => MetadataTrust::TypeOnly,
329        SurfaceHint::InitialGuess { trust, .. } | SurfaceHint::Constrained { trust, .. } => *trust,
330        SurfaceHint::ExactCandidate { .. } => MetadataTrust::Exact,
331    };
332    let metadata_seconds = metadata_started.map(|started| started.elapsed().as_secs_f64());
333    let result = match hint {
334        SurfaceHint::Unknown => best_model(
335            mesh,
336            selection,
337            scale,
338            options,
339            FitPath::GenericRecognition,
340            None,
341            ConstraintMask::default(),
342        ),
343        SurfaceHint::KnownType { surface_type } => fit_result(
344            mesh,
345            selection,
346            *surface_type,
347            None,
348            ConstraintMask::default(),
349            scale,
350            options,
351            FitPath::KnownTypeFit,
352            true,
353            None,
354        ),
355        SurfaceHint::InitialGuess { surface, trust } => {
356            if matches!(
357                trust,
358                crate::MetadataTrust::Exact | crate::MetadataTrust::StrongHint
359            ) {
360                let mut valid = selection.evaluate(mesh, *surface, options, FitPath::HintReused)?;
361                if selection.accepted(&valid, options, scale) {
362                    valid.diagnostics.reason = "supplied parameters validated".into();
363                    valid.diagnostics.metadata_trust = metadata_trust;
364                    valid.diagnostics.phase_timings.metadata_inspection_seconds = metadata_seconds;
365                    return Ok(valid);
366                }
367            }
368            fit_result(
369                mesh,
370                selection,
371                surface.surface_type(),
372                Some(*surface),
373                ConstraintMask::default(),
374                scale,
375                options,
376                FitPath::UnconstrainedRefinement,
377                true,
378                Some(*surface),
379            )
380        }
381        SurfaceHint::Constrained {
382            surface_type,
383            constraints,
384            ..
385        } => {
386            if constraints.initial.is_none() && constraints.fixed != ConstraintMask::default() {
387                return Err(RecognitionError::InvalidSelection(
388                    "fixed constraints require initial parameter values".into(),
389                ));
390            }
391            fit_result(
392                mesh,
393                selection,
394                *surface_type,
395                constraints.initial,
396                constraints.fixed,
397                scale,
398                options,
399                FitPath::ConstrainedRefinement,
400                true,
401                constraints.initial,
402            )
403        }
404        SurfaceHint::ExactCandidate { surface } => {
405            let mut valid =
406                selection.evaluate(mesh, *surface, options, FitPath::ExactCandidateReused)?;
407            if selection.accepted(&valid, options, scale) {
408                valid.diagnostics.fixed_parameters = ConstraintMask {
409                    origin_or_center: true,
410                    axis_or_normal: true,
411                    radius: true,
412                    major_radius: true,
413                    angle: true,
414                };
415                valid.diagnostics.reason =
416                    "exact source parameters validated and were reused unchanged".into();
417                valid.diagnostics.metadata_trust = metadata_trust;
418                valid.diagnostics.phase_timings.metadata_inspection_seconds = metadata_seconds;
419                return Ok(valid);
420            }
421            fit_result(
422                mesh,
423                selection,
424                surface.surface_type(),
425                Some(*surface),
426                ConstraintMask::default(),
427                scale,
428                options,
429                FitPath::HintRejectedFallback,
430                true,
431                Some(*surface),
432            )
433            .or_else(|_| {
434                best_model(
435                    mesh,
436                    selection,
437                    scale,
438                    options,
439                    FitPath::HintRejectedFallback,
440                    Some(*surface),
441                    ConstraintMask::default(),
442                )
443            })
444        }
445    }?;
446    Ok(with_metadata_diagnostics(
447        result,
448        metadata_trust,
449        metadata_seconds,
450    ))
451}
452
453fn with_metadata_diagnostics(
454    mut result: SurfaceFitResult,
455    trust: MetadataTrust,
456    elapsed_seconds: Option<f64>,
457) -> SurfaceFitResult {
458    result.diagnostics.metadata_trust = trust;
459    result.diagnostics.phase_timings.metadata_inspection_seconds = elapsed_seconds;
460    result
461}
462
463#[allow(clippy::too_many_arguments)]
464fn fit_result(
465    mesh: &AnalyzedMesh,
466    selection: Selection<'_>,
467    kind: SurfaceType,
468    initial: Option<AnalyticSurface>,
469    fixed: ConstraintMask,
470    scale: f64,
471    options: &RecognitionOptions,
472    path: FitPath,
473    skipped: bool,
474    supplied: Option<AnalyticSurface>,
475) -> Result<SurfaceFitResult, RecognitionError> {
476    let mut fitted = selection.fit(mesh, kind, initial, fixed, options, path)?;
477    let evaluation_started = options.collect_phase_timings.then(Instant::now);
478    if !selection.accepted(&fitted, options, scale) {
479        return Err(RecognitionError::FitFailed {
480            surface: Some(kind.name()),
481            reason: format!(
482                "residuals exceed tolerance (max {:.3e}, normal {:.3e} rad)",
483                fitted.metrics.max_error, fitted.metrics.max_normal_error
484            ),
485        });
486    }
487    fitted.diagnostics.generic_classification_skipped = skipped;
488    fitted.diagnostics.supplied_surface = supplied;
489    fitted.diagnostics.reason = "requested model fitted and validated".into();
490    fitted
491        .diagnostics
492        .phase_timings
493        .candidate_evaluation_seconds =
494        evaluation_started.map(|started| started.elapsed().as_secs_f64());
495    Ok(fitted)
496}
497
498fn best_model(
499    mesh: &AnalyzedMesh,
500    selection: Selection<'_>,
501    scale: f64,
502    options: &RecognitionOptions,
503    path: FitPath,
504    supplied: Option<AnalyticSurface>,
505    fixed: ConstraintMask,
506) -> Result<SurfaceFitResult, RecognitionError> {
507    best_model_impl(mesh, selection, scale, options, path, supplied, fixed, true)
508}
509
510#[allow(clippy::too_many_arguments)]
511fn best_model_impl(
512    mesh: &AnalyzedMesh,
513    selection: Selection<'_>,
514    scale: f64,
515    options: &RecognitionOptions,
516    path: FitPath,
517    supplied: Option<AnalyticSurface>,
518    fixed: ConstraintMask,
519    use_score_bounds: bool,
520) -> Result<SurfaceFitResult, RecognitionError> {
521    let evaluation_started = options.collect_phase_timings.then(Instant::now);
522    let tolerance = options.distance_tolerance + options.relative_tolerance * scale.max(1.0);
523    let mut candidates = Vec::new();
524    let mut rejected = Vec::new();
525    for kind in TYPES {
526        match selection.fit(mesh, kind, None, fixed, options, path) {
527            Ok(model) if selection.accepted(&model, options, scale) => {
528                let rank = model_rank(
529                    mesh,
530                    selection,
531                    kind,
532                    &model,
533                    tolerance,
534                    options.normal_tolerance,
535                );
536                candidates.push((rank, kind, model));
537            }
538            Ok(model) => rejected.push((
539                kind,
540                format!(
541                    "max distance {:.3e}, max normal {:.3e}",
542                    model.metrics.max_error, model.metrics.max_normal_error
543                ),
544            )),
545            Err(e) => rejected.push((kind, e.to_string())),
546        }
547        if use_score_bounds {
548            if let Some(bound) = unseen_model_score_lower_bound(kind) {
549                if let Some((best_index, rank, best_kind)) = candidates
550                    .iter()
551                    .enumerate()
552                    .min_by(|(_, a), (_, b)| compare_model_rank(a.0, a.1, b.0, b.1))
553                    .map(|(index, candidate)| (index, candidate.0, candidate.1))
554                {
555                    // A merely tolerance-valid simple model cannot rule out a
556                    // more complex carrier that fits at conditioned roundoff.
557                    // Once the current best is itself numerically exact, the
558                    // residual terms are non-negative and the ordinary score
559                    // lower bound is safe within the exact tier.
560                    if rank.numerically_exact && rank.score < bound {
561                        let (_, _, model) = candidates.remove(best_index);
562                        record_tolerance_valid_losers(&mut rejected, candidates, best_kind, rank);
563                        rejected.sort_by_key(|(candidate_kind, _)| *candidate_kind);
564                        let reason = format!(
565                            "{} selected after {} candidate(s) from a score below every unseen model's proven lower bound",
566                            best_kind.name(),
567                            kind as usize + 1,
568                        );
569                        return Ok(decorate_best_model(
570                            model,
571                            supplied,
572                            fixed,
573                            kind as usize + 1,
574                            rejected,
575                            &reason,
576                            evaluation_started.as_ref(),
577                        ));
578                    }
579                }
580            }
581        }
582    }
583    // A sphere is the R = 0 limit of the torus parameterization. Triangle
584    // centroids from a faceted sphere have latitude-dependent chord error, so
585    // an unconstrained torus can otherwise absorb that discretization with a
586    // tiny, numerically meaningless major radius. If both carriers pass every
587    // geometric gate and R is below the configured spatial resolution, the
588    // torus's extra axis and radius are not identifiable; retain the simpler
589    // sphere. This does not suppress a torus when the sphere fails validation.
590    let valid_sphere = candidates
591        .iter()
592        .any(|(_, kind, _)| *kind == SurfaceType::Sphere);
593    if valid_sphere {
594        let mut retained = Vec::with_capacity(candidates.len());
595        for (rank, kind, model) in candidates {
596            let sphere_limit_major_radius = match &model.surface {
597                AnalyticSurface::Torus(torus) if torus.major_radius <= tolerance => {
598                    Some(torus.major_radius)
599                }
600                _ => None,
601            };
602            if kind == SurfaceType::Torus {
603                if let Some(major_radius) = sphere_limit_major_radius {
604                    rejected.push((
605                        kind,
606                        format!(
607                            "tolerance-valid candidate excluded as a non-identifiable sphere-limit torus: score {:.6e}, numerically_exact={}, major radius {major_radius:.3e} <= spatial tolerance {tolerance:.3e}; rms distance {:.3e}, max distance {:.3e}, rms normal {:.3e}, max normal {:.3e}",
608                            rank.score,
609                            rank.numerically_exact,
610                            model.metrics.rms_error,
611                            model.metrics.max_error,
612                            model.metrics.rms_normal_error,
613                            model.metrics.max_normal_error,
614                        ),
615                    ));
616                    continue;
617                }
618            }
619            retained.push((rank, kind, model));
620        }
621        candidates = retained;
622    }
623    // A cylinder is the infinite-major-radius limit of a torus.  On a short
624    // faceted cylindrical patch an unconstrained torus can move its spine far
625    // away and absorb a few units of coordinate quantization while providing
626    // no second-curvature evidence in the normal field.  When both candidates
627    // pass every geometric gate, retain the simpler cylinder if the torus is
628    // not in a stronger numerical-exactness tier, does not improve RMS normal
629    // agreement, and its RMS-position gain is below the configured fraction
630    // of spatial resolution.  A genuine exact torus still outranks an inexact
631    // cylinder, and a measurably curved torus remains available to the
632    // observability safeguard or ordinary residual ranking.
633    if let Some((cylinder_rank, _, cylinder)) = candidates
634        .iter()
635        .find(|(_, kind, _)| *kind == SurfaceType::Cylinder)
636        .cloned()
637    {
638        let mut retained = Vec::with_capacity(candidates.len());
639        for (rank, kind, model) in candidates {
640            if kind == SurfaceType::Torus
641                && torus_is_non_identifiable_cylinder_limit(
642                    rank,
643                    &model.metrics,
644                    cylinder_rank,
645                    &cylinder.metrics,
646                    tolerance,
647                )
648            {
649                let rms_gain = (cylinder.metrics.rms_error - model.metrics.rms_error).max(0.0);
650                rejected.push((
651                    kind,
652                    format!(
653                        "tolerance-valid candidate excluded as a non-identifiable cylinder-limit torus: score {:.6e}, numerically_exact={}, RMS-position gain {rms_gain:.3e} <= {:.3e} (1% of spatial tolerance), torus RMS normal {:.3e} >= cylinder RMS normal {:.3e}; rms distance {:.3e}, max distance {:.3e}, max normal {:.3e}",
654                        rank.score,
655                        rank.numerically_exact,
656                        tolerance * numerical::TORUS_CYLINDER_LIMIT_RMS_GAIN_FRACTION,
657                        model.metrics.rms_normal_error,
658                        cylinder.metrics.rms_normal_error,
659                        model.metrics.rms_error,
660                        model.metrics.max_error,
661                        model.metrics.max_normal_error,
662                    ),
663                ));
664                continue;
665            }
666            retained.push((rank, kind, model));
667        }
668        candidates = retained;
669    }
670    // Complexity is a tie-breaker, not authority to discard observed second
671    // curvature. Apply this only after the non-identifiable cylinder-limit
672    // filter above: a surviving inexact torus may displace the cylinder when
673    // it Pareto-improves every reported residual and either resolves position
674    // at the existing spatial threshold or its oriented carrier-normal field
675    // is distinguishable on the exact same samples. Exact-tier ordering stays
676    // authoritative and therefore never enters this override.
677    if let (Some((torus_rank, _, torus)), Some((cylinder_rank, _, cylinder))) = (
678        candidates
679            .iter()
680            .find(|(_, kind, _)| *kind == SurfaceType::Torus)
681            .cloned(),
682        candidates
683            .iter()
684            .find(|(_, kind, _)| *kind == SurfaceType::Cylinder)
685            .cloned(),
686    ) {
687        let carrier_normal_disagreement = max_oriented_carrier_normal_disagreement(
688            mesh,
689            selection,
690            options.sampling,
691            &torus,
692            &cylinder,
693        );
694        if torus_observably_dominates_cylinder(
695            torus_rank,
696            &torus.metrics,
697            cylinder_rank,
698            &cylinder.metrics,
699            tolerance,
700            carrier_normal_disagreement,
701        ) {
702            let rms_position_gain = cylinder.metrics.rms_error - torus.metrics.rms_error;
703            candidates.retain(|(_, kind, _)| *kind != SurfaceType::Cylinder);
704            rejected.push((
705                SurfaceType::Cylinder,
706                format!(
707                    "tolerance-valid cylinder excluded because the torus observably resolves second curvature: RMS-position gain {rms_position_gain:.3e} (threshold {:.3e}), oriented carrier-normal disagreement {:.3e} (threshold {:.3e}); torus RMS/max distance {:.3e}/{:.3e} versus cylinder {:.3e}/{:.3e}; torus RMS/max normal {:.3e}/{:.3e} versus cylinder {:.3e}/{:.3e}",
708                    tolerance * numerical::TORUS_CYLINDER_LIMIT_RMS_GAIN_FRACTION,
709                    carrier_normal_disagreement.unwrap_or(0.0),
710                    numerical::EXACT_MODEL_NORMAL_ROUNDOFF_RADIANS,
711                    torus.metrics.rms_error,
712                    torus.metrics.max_error,
713                    cylinder.metrics.rms_error,
714                    cylinder.metrics.max_error,
715                    torus.metrics.rms_normal_error,
716                    torus.metrics.max_normal_error,
717                    cylinder.metrics.rms_normal_error,
718                    cylinder.metrics.max_normal_error,
719                ),
720            ));
721        }
722    }
723    candidates.sort_by(|a, b| compare_model_rank(a.0, a.1, b.0, b.1));
724    if candidates.is_empty() {
725        return Err(RecognitionError::FitFailed {
726            surface: None,
727            reason: "no analytic model passed distance, normal, and support gates".into(),
728        });
729    }
730    let (selected_rank, selected_kind, model) = candidates.remove(0);
731    record_tolerance_valid_losers(&mut rejected, candidates, selected_kind, selected_rank);
732    rejected.sort_by_key(|(candidate_kind, _)| *candidate_kind);
733    Ok(decorate_best_model(
734        model,
735        supplied,
736        fixed,
737        TYPES.len(),
738        rejected,
739        "best tolerance-valid model selected by numerical-exactness tier, residual score, and simplicity penalty",
740        evaluation_started.as_ref(),
741    ))
742}
743
744fn torus_observably_dominates_cylinder(
745    torus_rank: ModelRank,
746    torus: &crate::FitMetrics,
747    cylinder_rank: ModelRank,
748    cylinder: &crate::FitMetrics,
749    spatial_tolerance: f64,
750    carrier_normal_disagreement: Option<f64>,
751) -> bool {
752    !torus_rank.numerically_exact
753        && !cylinder_rank.numerically_exact
754        && torus.rms_error <= cylinder.rms_error
755        && torus.max_error <= cylinder.max_error
756        && torus.rms_normal_error <= cylinder.rms_normal_error
757        && torus.max_normal_error <= cylinder.max_normal_error
758        && ((cylinder.rms_error - torus.rms_error)
759            > spatial_tolerance * numerical::TORUS_CYLINDER_LIMIT_RMS_GAIN_FRACTION
760            || carrier_normal_disagreement.is_some_and(|disagreement| {
761                disagreement > numerical::EXACT_MODEL_NORMAL_ROUNDOFF_RADIANS
762                    && torus.rms_normal_error < cylinder.rms_normal_error
763            }))
764}
765
766fn max_oriented_carrier_normal_disagreement(
767    mesh: &AnalyzedMesh,
768    selection: Selection<'_>,
769    sampling: crate::SamplingMode,
770    left: &SurfaceFitResult,
771    right: &SurfaceFitResult,
772) -> Option<f64> {
773    let mut max_disagreement = 0.0_f64;
774    let mut samples = 0_usize;
775    let mut measure = |point| -> Option<()> {
776        let left_normal = left.surface.normal_at(point)? * left.orientation as f64;
777        let right_normal = right.surface.normal_at(point)? * right.orientation as f64;
778        if !left_normal.is_finite() || !right_normal.is_finite() {
779            return None;
780        }
781        let disagreement = left_normal.dot(right_normal).clamp(-1.0, 1.0).acos();
782        if !disagreement.is_finite() {
783            return None;
784        }
785        max_disagreement = max_disagreement.max(disagreement);
786        samples += 1;
787        Some(())
788    };
789
790    match selection {
791        Selection::Triangles(ids) => {
792            if matches!(
793                sampling,
794                crate::SamplingMode::TriangleCentroids | crate::SamplingMode::CentroidsAndVertices
795            ) {
796                for &id in ids {
797                    if mesh.triangles[id].area > 0.0 {
798                        measure(mesh.triangles[id].centroid)?;
799                    }
800                }
801            }
802            if matches!(
803                sampling,
804                crate::SamplingMode::Vertices | crate::SamplingMode::CentroidsAndVertices
805            ) {
806                let vertices: BTreeSet<_> = ids
807                    .iter()
808                    .filter(|&&id| mesh.triangles[id].area > 0.0)
809                    .flat_map(|&id| mesh.triangles[id].vertices)
810                    .collect();
811                for vertex in vertices {
812                    measure(mesh.vertices[vertex])?;
813                }
814            }
815        }
816        Selection::Vertices(vertices) => {
817            for &vertex in vertices {
818                measure(mesh.vertices[vertex])?;
819            }
820        }
821    }
822    (samples > 0).then_some(max_disagreement)
823}
824
825fn torus_is_non_identifiable_cylinder_limit(
826    torus_rank: ModelRank,
827    torus: &crate::FitMetrics,
828    cylinder_rank: ModelRank,
829    cylinder: &crate::FitMetrics,
830    spatial_tolerance: f64,
831) -> bool {
832    (!torus_rank.numerically_exact || cylinder_rank.numerically_exact)
833        && torus.rms_normal_error >= cylinder.rms_normal_error
834        && (cylinder.rms_error - torus.rms_error).max(0.0)
835            <= spatial_tolerance * numerical::TORUS_CYLINDER_LIMIT_RMS_GAIN_FRACTION
836}
837
838fn record_tolerance_valid_losers(
839    rejected: &mut Vec<(SurfaceType, String)>,
840    mut candidates: Vec<(ModelRank, SurfaceType, SurfaceFitResult)>,
841    selected_kind: SurfaceType,
842    selected_rank: ModelRank,
843) {
844    candidates.sort_by_key(|(_, kind, _)| *kind);
845    rejected.extend(candidates.into_iter().map(|(rank, kind, model)| {
846        (
847            kind,
848            format!(
849                "tolerance-valid candidate not selected: score {:.6e}, numerically_exact={} versus selected {} score {:.6e}, numerically_exact={}; rms distance {:.3e}, max distance {:.3e}, rms normal {:.3e}, max normal {:.3e}",
850                rank.score,
851                rank.numerically_exact,
852                selected_kind.name(),
853                selected_rank.score,
854                selected_rank.numerically_exact,
855                model.metrics.rms_error,
856                model.metrics.max_error,
857                model.metrics.rms_normal_error,
858                model.metrics.max_normal_error,
859            ),
860        )
861    }));
862}
863
864// BREP private tests: a948bced41f12b04
865
866fn unseen_model_score_lower_bound(after: SurfaceType) -> Option<f64> {
867    let degrees = match after {
868        SurfaceType::Plane => 4.0,
869        SurfaceType::Sphere => 5.0,
870        SurfaceType::Cylinder => 6.0,
871        SurfaceType::Cone => 8.0,
872        SurfaceType::Torus => return None,
873    };
874    Some(degrees * MODEL_COMPLEXITY_PENALTY_UNIT)
875}
876
877fn decorate_best_model(
878    mut answer: SurfaceFitResult,
879    supplied: Option<AnalyticSurface>,
880    fixed: ConstraintMask,
881    candidates_evaluated: usize,
882    rejected_competitors: Vec<(SurfaceType, String)>,
883    reason: &str,
884    evaluation_started: Option<&Instant>,
885) -> SurfaceFitResult {
886    answer.diagnostics.supplied_surface = supplied;
887    answer.diagnostics.fixed_parameters = fixed;
888    answer.diagnostics.generic_classification_skipped = false;
889    answer.diagnostics.reason = reason.into();
890    answer.diagnostics.candidates_evaluated = candidates_evaluated;
891    answer.diagnostics.rejected_competitors = rejected_competitors;
892    answer
893        .diagnostics
894        .phase_timings
895        .candidate_evaluation_seconds =
896        evaluation_started.map(|started| started.elapsed().as_secs_f64());
897    answer
898}
899
900fn model_rank(
901    mesh: &AnalyzedMesh,
902    selection: Selection<'_>,
903    kind: SurfaceType,
904    model: &SurfaceFitResult,
905    distance_tolerance: f64,
906    normal_tolerance: f64,
907) -> ModelRank {
908    let vertices: BTreeSet<_> = match selection {
909        Selection::Triangles(ids) => ids
910            .iter()
911            .flat_map(|&id| mesh.triangles[id].vertices)
912            .collect(),
913        Selection::Vertices(ids) => ids.iter().copied().collect(),
914    };
915    let coordinate_scale = vertices
916        .into_iter()
917        .map(|vertex| {
918            let point = mesh.vertices[vertex];
919            point.x.abs().max(point.y.abs()).max(point.z.abs())
920        })
921        .fold(1.0_f64, f64::max);
922    let position_roundoff =
923        numerical::EXACT_VERTEX_ROUNDOFF_MULTIPLIER * f64::EPSILON * coordinate_scale;
924    ModelRank {
925        numerically_exact: model.metrics.max_error <= position_roundoff
926            && model.metrics.max_normal_error <= numerical::EXACT_MODEL_NORMAL_ROUNDOFF_RADIANS,
927        score: model_selection_score(kind, &model.metrics, distance_tolerance, normal_tolerance),
928    }
929}
930
931fn model_selection_score(
932    kind: SurfaceType,
933    metrics: &crate::FitMetrics,
934    distance_tolerance: f64,
935    normal_tolerance: f64,
936) -> f64 {
937    let complexity = match kind {
938        SurfaceType::Plane => 3.,
939        SurfaceType::Sphere => 4.,
940        SurfaceType::Cylinder => 5.,
941        SurfaceType::Cone => 6.,
942        SurfaceType::Torus => 8.,
943    };
944    // Complexity regularizes candidates within the same numerical-exactness
945    // tier. `compare_model_rank` prevents that penalty from allowing a visibly
946    // worse simple carrier to override a carrier fitted at conditioned
947    // roundoff. This matters on tiny CAD patches where a sphere can pass a
948    // loose acceptance tolerance while the source torus fits exactly.
949    metrics.rms_error / distance_tolerance.max(numerical::SCORE_DISTANCE_DENOMINATOR_FLOOR)
950        + metrics.rms_normal_error / normal_tolerance.max(numerical::SCORE_NORMAL_DENOMINATOR_FLOOR)
951        + complexity * MODEL_COMPLEXITY_PENALTY_UNIT
952}
953
954// BREP private tests: 7e7864e47596fc01
955
956fn accepted_impl(
957    model: &SurfaceFitResult,
958    options: &RecognitionOptions,
959    scale: f64,
960    vertex_samples_only: bool,
961) -> bool {
962    let distance = options.distance_tolerance + options.relative_tolerance * scale.max(1.0);
963    // A trimmed CAD tessellation can contain pole/seam vertices whose supplied
964    // derivative normals are singular or locally reversed even though the
965    // vertices lie exactly on one carrier. In that machine-exact positional
966    // case, retain the normal field as a global orientation/coherence check:
967    // its area-weighted RMS must remain in the selected carrier normal's open
968    // hemisphere. A few zero-area or low-area antipodal samples may therefore
969    // survive, while a tangential, balanced, or generally incoherent field may
970    // not. This positional threshold is intentionally far stricter than the
971    // user tolerance, so ordinary noisy meshes cannot bypass the normal gate.
972    let numerical_position =
973        numerical::EXACT_VERTEX_ROUNDOFF_MULTIPLIER * f64::EPSILON * scale.max(1.0);
974    let exact_vertices_with_coherent_sense = vertex_samples_only
975        && model.metrics.max_error <= numerical_position
976        && model.metrics.rms_normal_error < std::f64::consts::FRAC_PI_2;
977    model.metrics.max_error <= distance
978        // Pole/seam slivers can have poorly conditioned chord normals even
979        // when their vertices lie on the exact CAD carrier. Preserve the true
980        // maximum in diagnostics, but use area-weighted RMS for acceptance.
981        && (model.metrics.rms_normal_error <= options.normal_tolerance
982            || exact_vertices_with_coherent_sense)
983        && model.metrics.support_triangles >= options.minimum_support
984        && model.metrics.supported_area >= options.minimum_support_area
985}
986fn to_region(fit: SurfaceFitResult, ids: Vec<usize>) -> SurfaceRegion {
987    SurfaceRegion {
988        surface: fit.surface,
989        orientation: fit.orientation,
990        triangle_indices: ids,
991        metrics: fit.metrics,
992        confidence: fit.confidence,
993        diagnostics: fit.diagnostics,
994    }
995}
996
997fn merge_disconnected_regions(
998    mesh: &AnalyzedMesh,
999    options: &RecognitionOptions,
1000    regions: &mut Vec<SurfaceRegion>,
1001) {
1002    let mut left = 0;
1003    while left < regions.len() {
1004        let mut right = left + 1;
1005        while right < regions.len() {
1006            if regions[left].surface.surface_type() != regions[right].surface.surface_type()
1007                || regions[left].orientation != regions[right].orientation
1008            {
1009                right += 1;
1010                continue;
1011            }
1012            let mut ids = regions[left].triangle_indices.clone();
1013            ids.extend_from_slice(&regions[right].triangle_indices);
1014            ids.sort_unstable();
1015            let hint = SurfaceHint::InitialGuess {
1016                surface: regions[left].surface,
1017                trust: crate::MetadataTrust::InitialGuess,
1018            };
1019            let Ok(mut fit) = reconstruct_analyzed(mesh, &ids, &hint, options) else {
1020                right += 1;
1021                continue;
1022            };
1023            if fit.orientation != regions[left].orientation {
1024                right += 1;
1025                continue;
1026            }
1027            fit.diagnostics
1028                .reason
1029                .push_str("; disconnected supports were jointly refitted and merged by request");
1030            regions[left] = to_region(fit, ids);
1031            regions.remove(right);
1032            // The enlarged region may now validate another disconnected patch.
1033            right = left + 1;
1034        }
1035        left += 1;
1036    }
1037}
1038
1039fn extract_component(
1040    mesh: &AnalyzedMesh,
1041    component: &[usize],
1042    options: &RecognitionOptions,
1043    out: &mut Vec<SurfaceRegion>,
1044) {
1045    if component.len() < options.minimum_support {
1046        return;
1047    }
1048    if let Ok(fit) = reconstruct_analyzed(mesh, component, &SurfaceHint::Unknown, options) {
1049        out.push(to_region(fit, component.to_vec()));
1050        return;
1051    }
1052    // CAD-aware hypothesis generation: fit small connected neighborhoods and
1053    // grow their geometric support through adjacency. This is deterministic
1054    // with a supplied seed and lets tangent analytic regions separate.
1055    let mut remaining: BTreeSet<usize> = component.iter().copied().collect();
1056    let mut rng = SplitMix(options.deterministic_seed.unwrap_or(0x52414e534143));
1057    while remaining.len() >= options.minimum_support {
1058        let ids: Vec<_> = remaining.iter().copied().collect();
1059        // One independently shuffled, without-replacement seed schedule per
1060        // primitive. At the full cap every remaining triangle is tried once
1061        // for every carrier; random-with-replacement sampling could waste
1062        // roughly a third of that budget on duplicate seeds.
1063        let seed_orders: Vec<_> = TYPES
1064            .iter()
1065            .map(|_| shuffled_seed_order(&ids, &mut rng))
1066            .collect();
1067        let mut best: Option<(usize, ModelRank, SurfaceFitResult, Vec<usize>)> = None;
1068        let attempt_cap = options.max_hypotheses.min(ids.len() * TYPES.len());
1069        let mut attempts_required = attempt_cap;
1070        let mut attempt = 0;
1071        let mut region_growth_seconds = options.collect_phase_timings.then_some(0.0);
1072        while attempt < attempts_required {
1073            let attempt_index = attempt;
1074            attempt += 1;
1075            let kind_index = attempt_index % TYPES.len();
1076            let round = attempt_index / TYPES.len();
1077            let seed = seed_orders[kind_index][round];
1078            let kind = TYPES[kind_index];
1079            // Generate each primitive from the smallest diverse triangle set
1080            // that supplies its fitter's required independent observations in
1081            // every public sampling mode. Final support is still grown and
1082            // validated against the complete connected region below. Keeping
1083            // the fitted sample primitive-specific avoids fitting every
1084            // candidate to the full diversity probe while preserving
1085            // aggressive rank/degeneracy rejection in the fitter.
1086            let sample_size = hypothesis_sample_triangles(kind);
1087            let mut probe_limit = HYPOTHESIS_DIVERSITY_PROBE_TRIANGLES
1088                .max(sample_size)
1089                .min(remaining.len());
1090            let mut probe_best: Option<(usize, ModelRank, SurfaceFitResult, Vec<usize>)> = None;
1091            loop {
1092                let probe = neighborhood(mesh, seed, &remaining, probe_limit);
1093                if probe.len() < sample_size {
1094                    break;
1095                }
1096                let sample = diverse_hypothesis_sample(mesh, seed, &probe, sample_size);
1097                let mut grew_beyond_probe = false;
1098                let mut reconstructed_probe = false;
1099                if let Ok(candidate) = fit_surface_with_path(
1100                    mesh,
1101                    &sample,
1102                    kind,
1103                    None,
1104                    ConstraintMask::default(),
1105                    options,
1106                    FitPath::GenericRecognition,
1107                ) {
1108                    let growth_started = options.collect_phase_timings.then(Instant::now);
1109                    let support = support_component(mesh, &ids, seed, candidate.surface, options);
1110                    if let (Some(total), Some(started)) =
1111                        (&mut region_growth_seconds, growth_started)
1112                    {
1113                        *total += started.elapsed().as_secs_f64();
1114                    }
1115                    grew_beyond_probe = support.len() > probe.len();
1116                    if support.len() >= options.minimum_support {
1117                        if let Ok(fit) = reconstruct_analyzed(
1118                            mesh,
1119                            &support,
1120                            &SurfaceHint::InitialGuess {
1121                                surface: candidate.surface,
1122                                trust: crate::MetadataTrust::InitialGuess,
1123                            },
1124                            options,
1125                        ) {
1126                            reconstructed_probe = true;
1127                            let area = fit.metrics.supported_area;
1128                            let support_scale = selection_scale(mesh, &support);
1129                            let support_tolerance = options.distance_tolerance
1130                                + options.relative_tolerance * support_scale.max(1.0);
1131                            let rank = model_rank(
1132                                mesh,
1133                                Selection::Triangles(&support),
1134                                fit.surface.surface_type(),
1135                                &fit,
1136                                support_tolerance,
1137                                options.normal_tolerance,
1138                            );
1139                            let replace =
1140                                probe_best.as_ref().is_none_or(|(count, old_rank, old, _)| {
1141                                    support.len() > *count
1142                                        || (support.len() == *count
1143                                            && (area > old.metrics.supported_area
1144                                                || (area == old.metrics.supported_area
1145                                                    && compare_model_rank(
1146                                                        rank,
1147                                                        fit.surface.surface_type(),
1148                                                        *old_rank,
1149                                                        old.surface.surface_type(),
1150                                                    ) == Ordering::Less)))
1151                                });
1152                            if replace {
1153                                probe_best = Some((support.len(), rank, fit, support));
1154                            }
1155                        }
1156                    }
1157                }
1158                let exhausted = probe.len() == remaining.len() || probe.len() < probe_limit;
1159                if (grew_beyond_probe && reconstructed_probe) || exhausted {
1160                    break;
1161                }
1162                // A candidate explaining no more triangles than its local
1163                // probe is under-observed (for example one planar strip of a
1164                // finely faceted cylinder). Expand only the diversity probe;
1165                // every fitter call remains primitive-minimal.
1166                probe_limit = probe_limit.saturating_mul(2).min(remaining.len());
1167            }
1168            let Some((_, rank, mut fit, support)) = probe_best else {
1169                continue;
1170            };
1171            fit.diagnostics.hypotheses_generated = attempt_index + 1;
1172            let area = fit.metrics.supported_area;
1173            let replace = best.as_ref().is_none_or(|(count, old_rank, old, _)| {
1174                support.len() > *count
1175                    || (support.len() == *count
1176                        && (area > old.metrics.supported_area
1177                            || (area == old.metrics.supported_area
1178                                && compare_model_rank(
1179                                    rank,
1180                                    fit.surface.surface_type(),
1181                                    *old_rank,
1182                                    old.surface.surface_type(),
1183                                ) == Ordering::Less)))
1184            });
1185            if replace {
1186                let support_fraction = support.len() as f64 / ids.len() as f64;
1187                attempts_required = attempts_required.min(required_seed_hypotheses(
1188                    options.confidence,
1189                    support_fraction,
1190                    TYPES.len(),
1191                    attempt_cap,
1192                ));
1193                attempts_required = attempts_required.max(attempt);
1194                best = Some((support.len(), rank, fit, support));
1195            }
1196        }
1197        let Some((_, _, mut fit, support)) = best else {
1198            break;
1199        };
1200        fit.diagnostics.phase_timings.region_growth_seconds = region_growth_seconds;
1201        for id in &support {
1202            remaining.remove(id);
1203        }
1204        out.push(to_region(fit, support));
1205    }
1206}
1207
1208fn shuffled_seed_order(ids: &[usize], rng: &mut SplitMix) -> Vec<usize> {
1209    let mut order = ids.to_vec();
1210    for upper in (2..=order.len()).rev() {
1211        let upper_u64 = upper as u64;
1212        // Rejection avoids modulo bias, retaining the stated without-
1213        // replacement sampling probability for any practical mesh size.
1214        let zone = (u64::MAX / upper_u64) * upper_u64;
1215        let index = loop {
1216            let value = rng.next();
1217            if value < zone {
1218                break (value % upper_u64) as usize;
1219            }
1220        };
1221        order.swap(upper - 1, index);
1222    }
1223    order
1224}
1225
1226/// Near-minimal triangle counts for generic primitive hypotheses.
1227///
1228/// `TriangleCentroids` yields exactly one observation per triangle, so these
1229/// counts match the primitive fitter's mathematical observation minima.
1230/// Vertex-containing sampling modes provide additional point/normal evidence
1231/// from the same compact connected patch.  Degenerate configurations are not
1232/// padded with arbitrary extra triangles: the fitter rejects them and RANSAC
1233/// tries another deterministic seed.
1234const fn hypothesis_sample_triangles(kind: SurfaceType) -> usize {
1235    match kind {
1236        SurfaceType::Plane => 3,
1237        SurfaceType::Sphere => 4,
1238        // Three centroid/normal observations are algebraically minimal, but
1239        // six are the near-minimal stable set on finely faceted CAD cylinders:
1240        // three can all lie in one numerically under-observed angular strip.
1241        SurfaceType::Cylinder => 6,
1242        SurfaceType::Cone => 4,
1243        SurfaceType::Torus => 6,
1244    }
1245}
1246
1247/// Select a deterministic, normal-diverse near-minimal subset from a compact
1248/// connected probe.  Merely taking adjacent triangles would let one planar
1249/// facet strip of a tessellated cylinder masquerade as a supported plane.
1250/// Normal diversity is therefore the primary farthest-point criterion;
1251/// centroid separation breaks ties and spans genuinely planar patches.
1252fn diverse_hypothesis_sample(
1253    mesh: &AnalyzedMesh,
1254    seed: usize,
1255    probe: &[usize],
1256    count: usize,
1257) -> Vec<usize> {
1258    debug_assert!(probe.contains(&seed));
1259    debug_assert!(count > 0 && probe.len() >= count);
1260    let mut selected = Vec::with_capacity(count);
1261    selected.push(seed);
1262    while selected.len() < count {
1263        let mut best: Option<(f64, f64, usize)> = None;
1264        for &candidate in probe {
1265            if selected.contains(&candidate) {
1266                continue;
1267            }
1268            let triangle = &mesh.triangles[candidate];
1269            let (normal_gap, spatial_gap) = selected.iter().fold(
1270                (f64::INFINITY, f64::INFINITY),
1271                |(normal_gap, spatial_gap), &chosen| {
1272                    let other = &mesh.triangles[chosen];
1273                    (
1274                        normal_gap
1275                            .min(1.0 - triangle.normal.dot(other.normal).abs().clamp(0.0, 1.0)),
1276                        spatial_gap.min((triangle.centroid - other.centroid).length_squared()),
1277                    )
1278                },
1279            );
1280            let key = (normal_gap, spatial_gap, std::cmp::Reverse(candidate));
1281            if best
1282                .as_ref()
1283                .is_none_or(|&(best_normal, best_spatial, best_id)| {
1284                    (normal_gap, spatial_gap, std::cmp::Reverse(candidate))
1285                        > (best_normal, best_spatial, std::cmp::Reverse(best_id))
1286                })
1287            {
1288                best = Some((key.0, key.1, candidate));
1289            }
1290        }
1291        selected.push(best.expect("probe has enough distinct triangles").2);
1292    }
1293    selected.sort_unstable();
1294    selected
1295}
1296
1297/// Conservative RANSAC stopping bound for round-robin primitive hypotheses.
1298/// Each primitive visits a separate without-replacement seed permutation.
1299/// For a region occupying fraction `w`, its exact hypergeometric miss
1300/// probability is no greater than the with-replacement bound `(1-w)^rounds`
1301/// used here. This controls the chance of visiting that support; candidate
1302/// rank and degeneracy checks still decide whether a visited seed is usable.
1303fn required_seed_hypotheses(
1304    confidence: f64,
1305    support_fraction: f64,
1306    kinds: usize,
1307    cap: usize,
1308) -> usize {
1309    let minimum = kinds.min(cap).max(1);
1310    if confidence <= 0.0 || support_fraction >= 1.0 {
1311        return minimum;
1312    }
1313    if support_fraction <= 0.0 || cap <= minimum {
1314        return cap.max(1);
1315    }
1316    let rounds = ((1.0 - confidence).ln() / (1.0 - support_fraction).ln())
1317        .ceil()
1318        .max(1.0) as usize;
1319    rounds.saturating_mul(kinds).clamp(minimum, cap)
1320}
1321
1322fn neighborhood(
1323    mesh: &AnalyzedMesh,
1324    seed: usize,
1325    allowed: &BTreeSet<usize>,
1326    limit: usize,
1327) -> Vec<usize> {
1328    let mut seen = BTreeSet::new();
1329    let mut queue = VecDeque::from([seed]);
1330    seen.insert(seed);
1331    while let Some(id) = queue.pop_front() {
1332        if seen.len() >= limit {
1333            break;
1334        }
1335        for n in mesh.triangles[id].neighbors.iter().flatten() {
1336            if allowed.contains(n) && seen.insert(*n) {
1337                queue.push_back(*n);
1338            }
1339        }
1340    }
1341    seen.into_iter().collect()
1342}
1343fn triangle_support(
1344    mesh: &AnalyzedMesh,
1345    id: usize,
1346    surface: AnalyticSurface,
1347    options: &RecognitionOptions,
1348) -> bool {
1349    let t = &mesh.triangles[id];
1350    let tolerance =
1351        options.distance_tolerance + options.relative_tolerance * mesh.diagonal.max(1.0);
1352    if t.vertices
1353        .iter()
1354        .any(|&v| surface.signed_distance(mesh.vertices[v]).abs() > tolerance)
1355    {
1356        return false;
1357    }
1358    let Some(normal) = surface.normal_at(t.centroid) else {
1359        return false;
1360    };
1361    t.normal.dot(normal).abs().clamp(-1.0, 1.0).acos() <= options.normal_tolerance
1362}
1363fn support_component(
1364    mesh: &AnalyzedMesh,
1365    candidates: &[usize],
1366    seed: usize,
1367    surface: AnalyticSurface,
1368    options: &RecognitionOptions,
1369) -> Vec<usize> {
1370    let allowed: BTreeSet<_> = candidates
1371        .iter()
1372        .copied()
1373        .filter(|&i| triangle_support(mesh, i, surface, options))
1374        .collect();
1375    if !allowed.contains(&seed) {
1376        return Vec::new();
1377    }
1378    let mut seen = BTreeSet::from([seed]);
1379    let mut queue = VecDeque::from([seed]);
1380    while let Some(id) = queue.pop_front() {
1381        for n in mesh.triangles[id].neighbors.iter().flatten() {
1382            if allowed.contains(n) && seen.insert(*n) {
1383                queue.push_back(*n);
1384            }
1385        }
1386    }
1387    seen.into_iter().collect()
1388}
1389struct SplitMix(u64);
1390impl SplitMix {
1391    fn next(&mut self) -> u64 {
1392        self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15);
1393        let mut z = self.0;
1394        z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
1395        z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
1396        z ^ (z >> 31)
1397    }
1398}
1399
1400// BREP private tests: b9e926aa1e3aa7e4