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}