use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum SamplingMode {
TriangleCentroids,
Vertices,
CentroidsAndVertices,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct RecognitionOptions {
pub distance_tolerance: f64,
pub relative_tolerance: f64,
pub normal_tolerance: f64,
pub minimum_support: usize,
pub minimum_support_area: f64,
pub confidence: f64,
pub deterministic_seed: Option<u64>,
pub max_hypotheses: usize,
pub max_refinement_iterations: usize,
pub feature_angle: f64,
pub respect_features: bool,
pub discover_regions: bool,
pub allow_disconnected_same_surface: bool,
pub collect_phase_timings: bool,
pub sampling: SamplingMode,
}
impl Default for RecognitionOptions {
fn default() -> Self {
Self {
distance_tolerance: 1.0e-6,
relative_tolerance: 1.0e-8,
normal_tolerance: 5.0_f64.to_radians(),
minimum_support: 6,
minimum_support_area: 0.0,
confidence: 0.999,
deterministic_seed: Some(0x4341_4452_414e_5341),
max_hypotheses: 512,
max_refinement_iterations: 160,
feature_angle: 30.0_f64.to_radians(),
respect_features: true,
discover_regions: true,
allow_disconnected_same_surface: false,
collect_phase_timings: false,
sampling: SamplingMode::CentroidsAndVertices,
}
}
}
impl RecognitionOptions {
pub(crate) fn validate(&self) -> Result<(), crate::RecognitionError> {
if !self.distance_tolerance.is_finite() || self.distance_tolerance <= 0.0 {
return Err(crate::RecognitionError::InvalidOptions(
"distance_tolerance must be finite and positive".into(),
));
}
if !self.relative_tolerance.is_finite() || self.relative_tolerance < 0.0 {
return Err(crate::RecognitionError::InvalidOptions(
"relative_tolerance must be finite and non-negative".into(),
));
}
if !(0.0..=std::f64::consts::PI).contains(&self.normal_tolerance) {
return Err(crate::RecognitionError::InvalidOptions(
"normal_tolerance must be in [0, pi]".into(),
));
}
if self.minimum_support == 0 {
return Err(crate::RecognitionError::InvalidOptions(
"minimum_support must be non-zero".into(),
));
}
if !self.minimum_support_area.is_finite() || self.minimum_support_area < 0.0 {
return Err(crate::RecognitionError::InvalidOptions(
"minimum_support_area must be finite and non-negative".into(),
));
}
if !(0.0..1.0).contains(&self.confidence) {
return Err(crate::RecognitionError::InvalidOptions(
"confidence must be in [0, 1)".into(),
));
}
if self.max_hypotheses == 0 || self.max_refinement_iterations == 0 {
return Err(crate::RecognitionError::InvalidOptions(
"iteration limits must be non-zero".into(),
));
}
if !(0.0..=std::f64::consts::PI).contains(&self.feature_angle) {
return Err(crate::RecognitionError::InvalidOptions(
"feature_angle must be finite and in [0, pi]".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RecognitionError;
fn invalid_message(options: RecognitionOptions) -> String {
match options.validate().unwrap_err() {
RecognitionError::InvalidOptions(message) => message,
error => panic!("expected invalid options, got {error:?}"),
}
}
#[test]
fn support_thresholds_are_validated() {
let mut options = RecognitionOptions {
minimum_support: 0,
..RecognitionOptions::default()
};
assert_eq!(
invalid_message(options.clone()),
"minimum_support must be non-zero"
);
for value in [-1.0, f64::INFINITY, f64::NAN] {
options.minimum_support = RecognitionOptions::default().minimum_support;
options.minimum_support_area = value;
assert_eq!(
invalid_message(options.clone()),
"minimum_support_area must be finite and non-negative"
);
}
}
#[test]
fn recognition_feature_angle_is_validated() {
for value in [-f64::EPSILON, std::f64::consts::PI + 1.0e-12, f64::NAN] {
let options = RecognitionOptions {
feature_angle: value,
..RecognitionOptions::default()
};
assert_eq!(
invalid_message(options),
"feature_angle must be finite and in [0, pi]"
);
}
for value in [0.0, std::f64::consts::PI] {
RecognitionOptions {
feature_angle: value,
..RecognitionOptions::default()
}
.validate()
.unwrap();
}
}
}