Skip to main content

axiolid_mesh_compile/
skin.rs

1//! A skin in a difference's or intersection's result, thinner than rounding
2//! (#400).
3//!
4//! The snap ([`crate::snap`]) closes a skin or sliver a rounding residue
5//! thick only where the faces are axis-aligned; a clip turned by a placement
6//! lands its faces on planes no `f64` coordinate can hold exactly, and the
7//! mesh boolean, which has no tolerance, leaves a lid between them. The
8//! result is closed and consistently wound, so the closure check labels it a
9//! Solid, yet two of its faces face each other within the rounding of the
10//! coordinates they were placed at: residue, not authored geometry (ADR 0096,
11//! D7). [`axiolid_inspect::thin_sheet`] finds that pair on the result, and the
12//! cut is refused by name instead of returned.
13//!
14//! A skin above the rounding but below the tolerance stays authored (ADR 0096,
15//! D7): the guard reads the rounding reach of [`crate::snap::reach`], never
16//! the tolerance. A union is not guarded here (#434).
17
18use axiolid_contracts::GeomError;
19use axiolid_core::{BooleanOperator, Scalar};
20use axiolid_inspect::ThinSheet;
21use axiolid_mesh::TriMesh;
22
23/// The start of the `GeomError::Degenerate` message a difference or
24/// intersection is refused with when its result holds a skin thinner than
25/// its coordinates' rounding (#400): two faces face each other on one plane,
26/// closer than the snap's reach.
27pub const SKIN_WITHIN_ROUNDING: &str = "boolean result holds a skin thinner than its coordinates' rounding: two of its faces face each other on one plane";
28
29/// The refusal for `found` in `mesh`, the result of `operator`, read at the
30/// rounding `reach`: the constant, then the two triangles, their offset, the
31/// reach and where the first one lies.
32pub(crate) fn refusal(
33    found: &ThinSheet,
34    mesh: &TriMesh,
35    reach: Scalar,
36    operator: BooleanOperator,
37) -> GeomError {
38    let centroid = mesh
39        .indices
40        .get(found.first * 3..found.first * 3 + 3)
41        .and_then(|corner| {
42            let at = |i: u32| mesh.positions.get(i as usize).copied();
43            Some((at(corner[0])? + at(corner[1])? + at(corner[2])?) / 3.0)
44        });
45    GeomError::Degenerate(format!(
46        "{SKIN_WITHIN_ROUNDING}: {operator:?} result, triangles {} and {} {:e} apart, \
47         within the rounding reach {reach:e}, near {centroid:?}",
48        found.first, found.second, found.offset
49    ))
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use axiolid_core::Point3;
56
57    #[test]
58    fn the_refusal_starts_with_the_constant_and_names_the_pair() {
59        // A fin folded back onto its face about a shared edge.
60        let mesh = TriMesh::new(
61            vec![
62                Point3::new(0.0, 0.0, 0.0),
63                Point3::new(1.0, 0.0, 0.0),
64                Point3::new(0.0, 1.0, 0.0),
65                Point3::new(0.0, 1.0, 1e-12),
66            ],
67            vec![0, 1, 2, 1, 0, 3],
68        );
69        let found = axiolid_inspect::thin_sheet(&mesh, 1e-9).expect("a fin on its face");
70        let GeomError::Degenerate(text) = refusal(&found, &mesh, 1e-9, BooleanOperator::Difference)
71        else {
72            panic!("a degenerate refusal");
73        };
74        assert!(text.starts_with(SKIN_WITHIN_ROUNDING), "{text}");
75        assert!(text.contains("triangles 0 and 1"), "{text}");
76        assert!(text.contains("Difference"), "{text}");
77    }
78}