use crate::topology::BrepSolid;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
#[serde(default)]
pub struct KernelTolerances {
pub convergence: f64,
pub model: f64,
pub intersection_fit: f64,
pub pcurve_consistency: f64,
pub export_knit: f64,
pub sew_search: f64,
pub sliver: f64,
pub angular: f64,
}
impl Default for KernelTolerances {
fn default() -> Self {
Self::for_scale(1.0, 1e-7)
}
}
impl KernelTolerances {
pub fn for_scale(scale: f64, model: f64) -> Self {
let scale = scale.abs().max(1.0);
let model = model.abs().max(1e-12);
Self {
convergence: (model * 1e-3).clamp(1e-12, model),
model,
intersection_fit: (model * 20.0).max(scale * 1e-8),
pcurve_consistency: (model * 200.0).max(4e-3),
export_knit: (model * 100.0).max(4e-3),
sew_search: (model * 20.0).max(scale * 1e-8),
sliver: (model * 4.0).max(scale * 1e-10),
angular: 1e-4,
}
}
pub fn for_solid(solid: &BrepSolid, model: f64) -> Self {
Self::for_scale(solid_scale(solid), model)
}
pub fn for_pair(first: &BrepSolid, second: &BrepSolid, model: f64) -> Self {
Self::for_scale(solid_scale(first).max(solid_scale(second)), model)
}
pub fn check(&self) -> Result<(), String> {
let positive = [
("convergence", self.convergence),
("model", self.model),
("intersection_fit", self.intersection_fit),
("pcurve_consistency", self.pcurve_consistency),
("export_knit", self.export_knit),
("sew_search", self.sew_search),
("sliver", self.sliver),
("angular", self.angular),
];
for (name, value) in positive {
if !value.is_finite() || value <= 0.0 {
return Err(format!("invalid {name} tolerance {value}"));
}
}
for ((first_name, first), (second_name, second)) in [
(("convergence", self.convergence), ("model", self.model)),
(
("model", self.model),
("intersection_fit", self.intersection_fit),
),
] {
if first > second {
return Err(format!(
"tolerance ladder violated: {first_name} ({first:.3e}) > \
{second_name} ({second:.3e})"
));
}
}
if self.sew_search < self.model {
return Err(format!(
"sew_search ({:.3e}) is below model tolerance ({:.3e})",
self.sew_search, self.model
));
}
Ok(())
}
pub fn spatial(&self) -> f64 {
self.model
}
pub fn heal_band(&self, diagonal: f64, k: f64) -> f64 {
self.model.max(diagonal.abs() * k.abs())
}
pub fn pcurve_acceptance(&self, diagonal: f64) -> f64 {
self.pcurve_consistency
.max(diagonal.abs() * PCURVE_ACCEPTANCE_REL)
}
}
pub const PCURVE_ACCEPTANCE_REL: f64 = 0.025;
pub fn parametric_tolerance(spatial: f64, derivative_magnitude: f64) -> f64 {
spatial.abs().max(1e-15) / derivative_magnitude.abs().max(1e-9)
}
pub fn surface_uv_tolerance(spatial: f64, du_magnitude: f64, dv_magnitude: f64) -> f64 {
parametric_tolerance(spatial, du_magnitude.abs().min(dv_magnitude.abs()))
}
pub const WELD_FLOOR: f64 = 1e-5;
pub const COMMIT_WELD_FLOOR: f64 = 1e-4;
pub fn assembler_weld(model: f64) -> f64 {
model.max(WELD_FLOOR)
}
pub fn commit_weld(search: f64) -> f64 {
search.max(COMMIT_WELD_FLOOR)
}
pub fn merge_scale(extent: f64) -> f64 {
1.0 + extent
}
pub const COINCIDENCE_DISTANCE_FLOOR: f64 = 1e-6;
pub fn solid_scale(solid: &BrepSolid) -> f64 {
if solid.vertices.is_empty() {
return 1.0;
}
let mut low = crate::Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut high = crate::Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for vertex in &solid.vertices {
low.x = low.x.min(vertex.point.x);
low.y = low.y.min(vertex.point.y);
low.z = low.z.min(vertex.point.z);
high.x = high.x.max(vertex.point.x);
high.y = high.y.max(vertex.point.y);
high.z = high.z.max(vertex.point.z);
}
high.sub(low).length().max(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{make_box_brep, Vec3};
#[test]
fn policy_is_scale_aware_and_ordered() {
let small = KernelTolerances::for_scale(1.0, 1e-7);
let large = KernelTolerances::for_scale(10_000.0, 1e-7);
small.check().unwrap();
large.check().unwrap();
assert!(large.sew_search > small.sew_search);
assert_eq!(large.model, small.model);
}
#[test]
fn spatial_returns_the_base_model_tolerance() {
let policy = KernelTolerances::for_scale(1.0, 1e-7);
assert_eq!(policy.spatial(), policy.model);
assert_eq!(policy.spatial(), 1e-7);
}
#[test]
fn heal_band_floors_model_by_a_size_relative_width() {
let policy = KernelTolerances::for_scale(1.0, 1e-7);
assert_eq!(policy.heal_band(1.0, 1e-9), policy.model);
assert!((policy.heal_band(1000.0, 1e-3) - 1.0).abs() < 1e-15);
}
#[test]
fn pcurve_acceptance_size_couples_above_the_absolute_floor() {
let policy = KernelTolerances::for_scale(1.0, 1e-7);
assert_eq!(policy.pcurve_acceptance(0.0), policy.pcurve_consistency);
assert_eq!(policy.pcurve_acceptance(0.1), policy.pcurve_consistency);
let gap = 0.018;
assert!(policy.pcurve_consistency < gap);
assert!(policy.pcurve_acceptance(0.897) > gap);
assert!((policy.pcurve_acceptance(2.0) - 2.0 * PCURVE_ACCEPTANCE_REL).abs() < 1e-15);
assert!(policy.pcurve_acceptance(1e6) >= policy.pcurve_consistency);
}
#[test]
fn solid_scale_uses_extent_not_distance_from_origin() {
let solid = make_box_brep(Vec3::new(1e6, 1e6, 1e6), 3.0, 4.0, 12.0).unwrap();
assert!((solid_scale(&solid) - 13.0).abs() < 1e-9);
}
#[test]
fn parametric_tolerance_scales_inversely_with_derivative() {
assert!((parametric_tolerance(1e-6, 10.0) - 1e-7).abs() < 1e-20);
assert!((parametric_tolerance(1e-6, 0.1) - 1e-5).abs() < 1e-18);
assert!(parametric_tolerance(1e-6, 0.0).is_finite());
}
#[test]
fn surface_uv_tolerance_uses_the_smaller_derivative() {
let band = surface_uv_tolerance(1e-6, 100.0, 2.0);
assert!((band - 5e-7).abs() < 1e-18);
}
#[test]
fn inverted_policy_is_rejected() {
let policy = KernelTolerances {
convergence: 1e-2,
model: 1e-4,
..KernelTolerances::default()
};
assert!(policy.check().is_err());
}
}