Skip to main content

brep_ransac/
surface.rs

1use crate::numerical::scalar;
2use crate::Vec3;
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
6/// The supported analytic primitive families.
7pub enum SurfaceType {
8    /// A plane.
9    Plane,
10    /// A sphere.
11    Sphere,
12    /// A circular cylinder.
13    Cylinder,
14    /// A right circular cone.
15    Cone,
16    /// A circular torus.
17    Torus,
18}
19
20impl SurfaceType {
21    /// Returns the lowercase human-readable primitive name.
22    pub fn name(self) -> &'static str {
23        match self {
24            Self::Plane => "plane",
25            Self::Sphere => "sphere",
26            Self::Cylinder => "cylinder",
27            Self::Cone => "cone",
28            Self::Torus => "torus",
29        }
30    }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
34/// A plane represented by a point and unit normal.
35pub struct PlaneSurface {
36    /// A point on the plane.
37    pub origin: Vec3,
38    /// The plane's unit normal.
39    pub normal: Vec3,
40}
41#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
42/// A sphere represented by its center and radius.
43pub struct SphereSurface {
44    /// The sphere center.
45    pub center: Vec3,
46    /// The positive sphere radius.
47    pub radius: f64,
48}
49#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
50/// An infinite circular cylinder.
51pub struct CylinderSurface {
52    /// A point on the cylinder axis.
53    pub axis_origin: Vec3,
54    /// The unit direction of the cylinder axis.
55    pub axis: Vec3,
56    /// The positive cylinder radius.
57    pub radius: f64,
58}
59#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
60/// One nappe of a right circular cone.
61pub struct ConeSurface {
62    /// The cone apex.
63    pub apex: Vec3,
64    /// Unit axis directed into the represented nappe.
65    pub axis: Vec3,
66    /// Angle between the axis and surface generators, in radians.
67    pub half_angle: f64,
68}
69#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
70/// A circular torus represented by its center, axis, and radii.
71pub struct TorusSurface {
72    /// Center of the torus's generating circle.
73    pub center: Vec3,
74    /// Unit normal of the generating circle's plane.
75    pub axis: Vec3,
76    /// Radius from `center` to the centerline of the tube.
77    pub major_radius: f64,
78    /// Radius of the torus tube.
79    pub minor_radius: f64,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
83#[serde(tag = "type", content = "parameters")]
84/// Parameters for one supported analytic surface.
85pub enum AnalyticSurface {
86    /// A planar surface.
87    Plane(PlaneSurface),
88    /// A spherical surface.
89    Sphere(SphereSurface),
90    /// A cylindrical surface.
91    Cylinder(CylinderSurface),
92    /// A conical surface.
93    Cone(ConeSurface),
94    /// A toroidal surface.
95    Torus(TorusSurface),
96}
97
98impl AnalyticSurface {
99    /// Returns this surface's primitive family.
100    pub fn surface_type(self) -> SurfaceType {
101        match self {
102            Self::Plane(_) => SurfaceType::Plane,
103            Self::Sphere(_) => SurfaceType::Sphere,
104            Self::Cylinder(_) => SurfaceType::Cylinder,
105            Self::Cone(_) => SurfaceType::Cone,
106            Self::Torus(_) => SurfaceType::Torus,
107        }
108    }
109    /// Evaluates the surface's signed implicit distance-like residual at `point`.
110    ///
111    /// For normalized valid parameters, its magnitude is the geometric distance
112    /// for planes, spheres, cylinders, and tori; the cone expression is its
113    /// signed meridian distance.
114    pub fn signed_distance(self, point: Vec3) -> f64 {
115        match self {
116            Self::Plane(s) => (point - s.origin).dot(s.normal),
117            Self::Sphere(s) => (point - s.center).length() - s.radius,
118            Self::Cylinder(s) => {
119                let q = point - s.axis_origin;
120                (q - s.axis * q.dot(s.axis)).length() - s.radius
121            }
122            Self::Cone(s) => {
123                let q = point - s.apex;
124                let h = q.dot(s.axis);
125                let radial = (q - s.axis * h).length();
126                radial * s.half_angle.cos() - h * s.half_angle.sin()
127            }
128            Self::Torus(s) => {
129                let q = point - s.center;
130                let z = q.dot(s.axis);
131                let rho = (q - s.axis * z).length();
132                ((rho - s.major_radius).powi(2) + z * z).sqrt() - s.minor_radius
133            }
134        }
135    }
136    /// Returns the analytic unit normal at `point`, if it is defined there.
137    pub fn normal_at(self, point: Vec3) -> Option<Vec3> {
138        match self {
139            Self::Plane(s) => Some(s.normal),
140            Self::Sphere(s) => (point - s.center).normalized(),
141            Self::Cylinder(s) => {
142                let q = point - s.axis_origin;
143                (q - s.axis * q.dot(s.axis)).normalized()
144            }
145            Self::Cone(s) => {
146                let q = point - s.apex;
147                let h = q.dot(s.axis);
148                let radial = (q - s.axis * h).normalized()?;
149                (radial * s.half_angle.cos() - s.axis * s.half_angle.sin()).normalized()
150            }
151            Self::Torus(s) => {
152                let q = point - s.center;
153                let z = q.dot(s.axis);
154                let radial = q - s.axis * z;
155                let rho = radial.length();
156                if rho <= scalar::MIN_NORMALIZABLE_NORM {
157                    return None;
158                }
159                let tube = radial * (1.0 - s.major_radius / rho) + s.axis * z;
160                tube.normalized()
161            }
162        }
163    }
164    /// Returns whether all carrier parameters are finite and geometrically valid.
165    pub fn is_valid(self) -> bool {
166        match self {
167            Self::Plane(s) => s.origin.is_finite() && unit(s.normal),
168            Self::Sphere(s) => s.center.is_finite() && s.radius.is_finite() && s.radius > 0.0,
169            Self::Cylinder(s) => {
170                s.axis_origin.is_finite() && unit(s.axis) && s.radius.is_finite() && s.radius > 0.0
171            }
172            Self::Cone(s) => {
173                s.apex.is_finite()
174                    && unit(s.axis)
175                    && s.half_angle.is_finite()
176                    && s.half_angle > 0.0
177                    && s.half_angle < std::f64::consts::FRAC_PI_2
178            }
179            Self::Torus(s) => {
180                s.center.is_finite()
181                    && unit(s.axis)
182                    && s.major_radius.is_finite()
183                    && s.minor_radius.is_finite()
184                    && s.major_radius > 0.0
185                    && s.minor_radius > 0.0
186            }
187        }
188    }
189    /// Choose deterministic orientation/gauge conventions without changing
190    /// the represented infinite surface.
191    pub fn canonicalized(self, centroid: Vec3) -> Self {
192        match self {
193            Self::Plane(mut s) => {
194                s.normal = s.normal.normalized().unwrap_or(s.normal).canonicalized();
195                s.origin = centroid + s.normal * (s.origin - centroid).dot(s.normal);
196                Self::Plane(s)
197            }
198            Self::Sphere(s) => Self::Sphere(s),
199            Self::Cylinder(mut s) => {
200                s.axis = s.axis.normalized().unwrap_or(s.axis).canonicalized();
201                s.axis_origin += s.axis * (centroid - s.axis_origin).dot(s.axis);
202                Self::Cylinder(s)
203            }
204            Self::Cone(mut s) => {
205                s.axis = s.axis.normalized().unwrap_or(s.axis);
206                Self::Cone(s)
207            }
208            Self::Torus(mut s) => {
209                s.axis = s.axis.normalized().unwrap_or(s.axis).canonicalized();
210                Self::Torus(s)
211            }
212        }
213    }
214}
215fn unit(v: Vec3) -> bool {
216    v.is_finite() && (v.length() - 1.0).abs() <= scalar::UNIT_LENGTH_TOLERANCE
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
220/// Describes how strongly imported metadata should influence fitting.
221pub enum MetadataTrust {
222    /// Parameters are asserted to describe the exact source carrier.
223    Exact,
224    /// Parameters are a strong prior but must still be validated.
225    StrongHint,
226    /// Parameters only initialize numerical refinement.
227    InitialGuess,
228    /// Only the primitive family is trusted.
229    TypeOnly,
230    /// No trust information was supplied.
231    Unknown,
232}
233
234impl Default for MetadataTrust {
235    fn default() -> Self {
236        Self::Unknown
237    }
238}
239
240/// Parameter-level constraints. Vector fields constrain all three components;
241/// axis origins/centers still use the conventional along-axis gauge freedom.
242#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
243pub struct ConstraintMask {
244    /// Fix the plane origin, sphere/torus center, cylinder axis origin, or cone apex.
245    pub origin_or_center: bool,
246    /// Fix the axis or normal direction.
247    pub axis_or_normal: bool,
248    /// Fix the ordinary radius or torus minor radius.
249    pub radius: bool,
250    /// Fix the torus major radius.
251    pub major_radius: bool,
252    /// Fix the cone half-angle.
253    pub angle: bool,
254}
255
256#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
257/// Initial surface parameters and the subset held fixed during fitting.
258pub struct SurfaceConstraints {
259    /// Initial carrier parameters, when numeric values are available.
260    pub initial: Option<AnalyticSurface>,
261    /// Parameter groups that refinement must preserve.
262    pub fixed: ConstraintMask,
263}
264
265#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
266/// Optional source knowledge used to guide surface recognition.
267pub enum SurfaceHint {
268    /// No prior surface information is available.
269    Unknown,
270    /// The primitive family is known but its parameters are not.
271    KnownType {
272        /// The known primitive family.
273        surface_type: SurfaceType,
274    },
275    /// Numeric parameters are available as a refinement starting point.
276    InitialGuess {
277        /// Initial carrier parameters.
278        surface: AnalyticSurface,
279        /// Confidence assigned to the supplied parameters.
280        trust: MetadataTrust,
281    },
282    /// The family is known and selected parameter groups may be fixed.
283    Constrained {
284        /// The required primitive family.
285        surface_type: SurfaceType,
286        /// Initial values and fixed-parameter mask.
287        constraints: SurfaceConstraints,
288        /// Confidence assigned to the supplied constraints.
289        trust: MetadataTrust,
290    },
291    /// A complete carrier that may be reused after validation.
292    ExactCandidate {
293        /// The proposed exact carrier.
294        surface: AnalyticSurface,
295    },
296}
297
298#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
299/// The recognition or refinement route that produced a fit.
300pub enum FitPath {
301    /// The primitive family and parameters were recognized without metadata.
302    GenericRecognition,
303    /// Parameters were fitted for a metadata-specified primitive family.
304    KnownTypeFit,
305    /// Supplied hint parameters were accepted without refinement.
306    HintReused,
307    /// A supplied exact candidate was accepted without refinement.
308    ExactCandidateReused,
309    /// Refinement honored one or more fixed parameter groups.
310    ConstrainedRefinement,
311    /// Supplied parameters initialized unconstrained refinement.
312    UnconstrainedRefinement,
313    /// A rejected hint was followed by generic recognition.
314    HintRejectedFallback,
315}
316
317#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
318/// Residual and support measurements for a fitted surface.
319pub struct FitMetrics {
320    /// Root-mean-square positional residual.
321    pub rms_error: f64,
322    /// Maximum absolute positional residual.
323    pub max_error: f64,
324    /// Root-mean-square normal-angle residual, in radians.
325    pub rms_normal_error: f64,
326    /// Maximum normal-angle residual, in radians.
327    pub max_normal_error: f64,
328    /// Number of supporting triangles.
329    pub support_triangles: usize,
330    /// Total area of supporting triangles.
331    pub supported_area: f64,
332}
333
334/// Residual-only snapshot used to compare a supplied/initialized carrier with
335/// the final refined carrier without duplicating support bookkeeping.
336#[derive(Clone, Copy, Debug, Default, PartialEq, Deserialize, Serialize)]
337pub struct GeometricError {
338    /// Root-mean-square positional residual.
339    pub rms_position: f64,
340    /// Maximum absolute positional residual.
341    pub max_position: f64,
342    /// Root-mean-square normal-angle residual, in radians.
343    pub rms_normal_radians: f64,
344    /// Maximum normal-angle residual, in radians.
345    pub max_normal_radians: f64,
346}
347
348impl From<&FitMetrics> for GeometricError {
349    fn from(metrics: &FitMetrics) -> Self {
350        Self {
351            rms_position: metrics.rms_error,
352            max_position: metrics.max_error,
353            rms_normal_radians: metrics.rms_normal_error,
354            max_normal_radians: metrics.max_normal_error,
355        }
356    }
357}
358
359/// Gauge-aware change from an initial/supplied carrier to its final carrier.
360/// Fields that do not apply to a primitive remain `None`.
361#[derive(Clone, Copy, Debug, Default, PartialEq, Deserialize, Serialize)]
362#[serde(default)]
363pub struct SurfaceParameterDelta {
364    /// Gauge-aware displacement of an origin, center, axis line, or apex.
365    pub origin_or_center: Option<f64>,
366    /// Change in axis or normal direction, in radians.
367    pub axis_or_normal_radians: Option<f64>,
368    /// Change in the primitive radius, or torus minor radius.
369    pub radius: Option<f64>,
370    /// Change in torus major radius.
371    pub major_radius: Option<f64>,
372    /// Change in torus minor radius.
373    pub minor_radius: Option<f64>,
374    /// Change in cone half-angle, in radians.
375    pub angle_radians: Option<f64>,
376}
377
378impl SurfaceParameterDelta {
379    /// Compare compatible carrier parameterizations. Returns `None` when the
380    /// primitive types differ. Axis/normal angles respect each carrier's gauge:
381    /// cone axes are directed, while plane, cylinder, and torus directions are
382    /// sign-equivalent.
383    pub fn between(initial: AnalyticSurface, refined: AnalyticSurface) -> Option<Self> {
384        fn angle(a: Vec3, b: Vec3, oriented: bool) -> f64 {
385            let dot = a.dot(b).clamp(-1.0, 1.0);
386            (if oriented { dot } else { dot.abs() }).acos()
387        }
388        fn line_distance(p: Vec3, a: Vec3, q: Vec3, b: Vec3) -> f64 {
389            let cross = a.cross(b);
390            let length = cross.length();
391            if length > scalar::PARALLEL_LINE_CROSS_NORM {
392                (q - p).dot(cross).abs() / length
393            } else {
394                let delta = q - p;
395                (delta - a * delta.dot(a)).length()
396            }
397        }
398        Some(match (initial, refined) {
399            (AnalyticSurface::Plane(a), AnalyticSurface::Plane(b)) => Self {
400                origin_or_center: Some((b.origin - a.origin).dot(a.normal).abs()),
401                axis_or_normal_radians: Some(angle(a.normal, b.normal, false)),
402                ..Default::default()
403            },
404            (AnalyticSurface::Sphere(a), AnalyticSurface::Sphere(b)) => Self {
405                origin_or_center: Some(a.center.distance(b.center)),
406                radius: Some((a.radius - b.radius).abs()),
407                ..Default::default()
408            },
409            (AnalyticSurface::Cylinder(a), AnalyticSurface::Cylinder(b)) => Self {
410                origin_or_center: Some(line_distance(a.axis_origin, a.axis, b.axis_origin, b.axis)),
411                axis_or_normal_radians: Some(angle(a.axis, b.axis, false)),
412                radius: Some((a.radius - b.radius).abs()),
413                ..Default::default()
414            },
415            (AnalyticSurface::Cone(a), AnalyticSurface::Cone(b)) => Self {
416                origin_or_center: Some(a.apex.distance(b.apex)),
417                axis_or_normal_radians: Some(angle(a.axis, b.axis, true)),
418                angle_radians: Some((a.half_angle - b.half_angle).abs()),
419                ..Default::default()
420            },
421            (AnalyticSurface::Torus(a), AnalyticSurface::Torus(b)) => Self {
422                origin_or_center: Some(a.center.distance(b.center)),
423                axis_or_normal_radians: Some(angle(a.axis, b.axis, false)),
424                radius: Some((a.minor_radius - b.minor_radius).abs()),
425                major_radius: Some((a.major_radius - b.major_radius).abs()),
426                minor_radius: Some((a.minor_radius - b.minor_radius).abs()),
427                ..Default::default()
428            },
429            _ => return None,
430        })
431    }
432}
433
434/// Optional wall-clock phase measurements. Timing is disabled by default so
435/// deterministic results remain bit-for-bit comparable.
436#[derive(Clone, Copy, Debug, Default, PartialEq, Deserialize, Serialize)]
437#[serde(default)]
438pub struct PhaseTimings {
439    /// Time spent inspecting and validating metadata.
440    pub metadata_inspection_seconds: Option<f64>,
441    /// Time spent generating candidate carriers.
442    pub candidate_generation_seconds: Option<f64>,
443    /// Time spent evaluating candidate residuals and support.
444    pub candidate_evaluation_seconds: Option<f64>,
445    /// Time spent discovering or growing connected regions.
446    pub region_growth_seconds: Option<f64>,
447    /// Time spent numerically refining carrier parameters.
448    pub refinement_seconds: Option<f64>,
449    /// Time spent validating the final carrier.
450    pub validation_seconds: Option<f64>,
451}
452
453#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
454/// Provenance and execution details for a fitted surface.
455pub struct FitDiagnostics {
456    /// The recognition or refinement path used.
457    pub path: FitPath,
458    /// Carrier supplied by metadata, when one was available.
459    pub supplied_surface: Option<AnalyticSurface>,
460    /// Supplied parameter groups held fixed during refinement.
461    pub fixed_parameters: ConstraintMask,
462    /// Whether numerical refinement changed carrier parameters.
463    pub parameters_refined: bool,
464    /// Whether metadata made generic primitive classification unnecessary.
465    pub generic_classification_skipped: bool,
466    /// Whether exact supplied parameters were returned unchanged.
467    pub exact_parameters_reused: bool,
468    /// Number of hypotheses generated during recognition.
469    pub hypotheses_generated: usize,
470    /// Number of candidate carriers evaluated.
471    pub candidates_evaluated: usize,
472    /// Human-readable summary of why this path or result was selected.
473    pub reason: String,
474    /// Primitive competitors rejected during selection and their reasons.
475    pub rejected_competitors: Vec<(SurfaceType, String)>,
476    /// Trust attached to the selected input path (`Unknown` and `TypeOnly` are
477    /// recorded explicitly even though they carry no numeric prior).
478    #[serde(default)]
479    pub metadata_trust: MetadataTrust,
480    /// Geometric residual before refinement when an initial carrier exists.
481    #[serde(default)]
482    pub initial_error: Option<GeometricError>,
483    /// Geometric residual of the returned carrier.
484    #[serde(default)]
485    pub refined_error: Option<GeometricError>,
486    /// Gauge-aware parameter change from initial/supplied to returned carrier.
487    #[serde(default)]
488    pub parameter_delta: Option<SurfaceParameterDelta>,
489    #[serde(default)]
490    /// Optional wall-clock timing measurements by recognition phase.
491    pub phase_timings: PhaseTimings,
492}
493
494impl Default for FitDiagnostics {
495    fn default() -> Self {
496        Self {
497            path: FitPath::GenericRecognition,
498            supplied_surface: None,
499            fixed_parameters: ConstraintMask::default(),
500            parameters_refined: false,
501            generic_classification_skipped: false,
502            exact_parameters_reused: false,
503            hypotheses_generated: 0,
504            candidates_evaluated: 0,
505            reason: String::new(),
506            rejected_competitors: Vec::new(),
507            metadata_trust: MetadataTrust::Unknown,
508            initial_error: None,
509            refined_error: None,
510            parameter_delta: None,
511            phase_timings: PhaseTimings::default(),
512        }
513    }
514}
515
516#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
517/// The fitted carrier and quality information for one requested selection.
518pub struct SurfaceFitResult {
519    /// The fitted analytic carrier.
520    pub surface: AnalyticSurface,
521    /// Carrier orientation relative to mesh winding, as `-1` or `1`.
522    pub orientation: i8,
523    /// Residual and support measurements for the fit.
524    pub metrics: FitMetrics,
525    /// Normalized evidence score for the returned fit.
526    pub confidence: f64,
527    /// Provenance and execution details for the fit.
528    pub diagnostics: FitDiagnostics,
529}
530
531#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
532/// A recognized surface and the mesh triangles assigned to it.
533pub struct SurfaceRegion {
534    /// The region's analytic carrier.
535    pub surface: AnalyticSurface,
536    /// Carrier orientation relative to mesh winding, as `-1` or `1`.
537    pub orientation: i8,
538    /// Indices of triangles assigned to this region.
539    pub triangle_indices: Vec<usize>,
540    /// Residual and support measurements for the region.
541    pub metrics: FitMetrics,
542    /// Normalized evidence score for the region's fit.
543    pub confidence: f64,
544    /// Provenance and execution details for the region's fit.
545    pub diagnostics: FitDiagnostics,
546}
547
548#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
549/// Why a metadata-associated triangle subset remained unresolved after both
550/// metadata-guided reconstruction and generic analytic fallback failed.
551pub struct UnresolvedRegionDiagnostic {
552    /// Still-unresolved triangles from the metadata subset.
553    pub triangle_indices: Vec<usize>,
554    /// Stable source-system face identifier, when supplied.
555    pub source_face_id: Option<u64>,
556    /// Human-readable source-system face name, when supplied.
557    pub source_face_name: Option<String>,
558    /// Stable source-system surface identifier, when supplied.
559    pub source_surface_id: Option<String>,
560    /// Explicit metadata-validation or reconstruction failure followed by the
561    /// fact that generic analytic extraction left these triangles unresolved.
562    pub reason: String,
563}
564
565#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
566/// All recognized regions and triangles left unresolved by discovery.
567pub struct RecognitionResult {
568    /// Successfully recognized surface regions.
569    pub regions: Vec<SurfaceRegion>,
570    /// Input triangle indices not assigned to any recognized region.
571    pub unresolved_triangles: Vec<usize>,
572    /// Failure provenance for unresolved subsets that carried source metadata.
573    #[serde(default)]
574    pub unresolved_diagnostics: Vec<UnresolvedRegionDiagnostic>,
575}