Tests: a face's flat area may differ from its surface's by the sails its boundary chords bridge

a_modelled_thread_in_a_bore_meshes_inside_its_block held each face's triangle area to within 5%
of its surface's area. At the 0.1 mm tolerance, face 18 failed that: 1.777 mm^2 of triangles
over 1.522 mm^2 of surface. Face 18 is the groove's crest band where it runs out through the
block's end, a wedge at most 0.44 mm wide. Its boundary sides are the helix edge's chords, 1.37
mm long and sagging 0.062 mm (inside the tolerance). A flat triangle on such a chord bridges the
sail between the chord and the edge instead of following it, and the band is no wider than the
sag. The excess, 0.255 mm^2, is less than the sails along the face's boundary (sum of 2/3 L sag
= 0.62 mm^2). Every triangle is within the tolerance: the chord check passes. No triangulation
of the inside changes this; the boundary nodes are the shared edge nodes.

The check now allows 5% plus the boundary's sails at the tolerance, 2/3 L tol per boundary side.
A zigzag that multiplies a face's area, which this test exists to catch, is still far outside
that. On the run-out root band the bound allows about 40% at 0.1 mm and 5% plus 4 mm^2 at
0.01 mm, against the former 6x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 03:30:44 -04:00
committed by Irakli Kourtis
parent 71ea626d0a
commit 7ec4b7212d
+26 -7
View File
@@ -470,11 +470,16 @@ fn iso_groove(p: f64, d: f64, wall: f64, internal: bool) -> [(f64, f64); 4] {
}
/// Each face's triangles, in 3D, cover as much area as its surface over the triangles' own
/// parameters, within 5% (a coarse tolerance on a small curved face, a few percent): nothing
/// zigzags across the face, which multiplies its area. The surface's area by the midpoint rule
/// on sixteen pieces of each parameter triangle.
/// parameters, within 5% (a coarse tolerance on a small curved face, a few percent) and the
/// sails along its boundary: nothing zigzags across the face, which multiplies its area. The
/// surface's area by the midpoint rule on sixteen pieces of each parameter triangle. A face's
/// boundary sides are its edges' chords, each up to the chord tolerance `tol` off its edge, and
/// between a chord of length L and its edge lies a sail of surface of at most 2/3 L tol (a
/// parabolic segment) that flat triangles bridge rather than follow: a face no wider than its
/// chords sag (the thread's crest band where it runs out through the block's end, 0.06 mm sag
/// at 0.1) takes up to that much more or less area however it is triangulated inside.
#[cfg(feature = "smlib")]
fn assert_faces_lie_flat(name: &str, plan: &solidcraft_geom::tess::TessPlan) {
fn assert_faces_lie_flat(name: &str, plan: &solidcraft_geom::tess::TessPlan, tol: f64) {
for fp in &plan.faces {
let Some(s) = fp.surface.and_then(|i| plan.surfaces.get(i as usize)) else { continue };
let at = |i: u32| {
@@ -504,7 +509,21 @@ fn assert_faces_lie_flat(name: &str, plan: &solidcraft_geom::tess::TessPlan) {
}
}
}
assert!((flat - surface).abs() <= 0.05 * surface, "{name}: face {} has {flat} mm² of triangles over {surface} mm² of surface", fp.face);
// The boundary: the sides of exactly one triangle.
let mut sides: HashMap<(u32, u32), u32> = HashMap::new();
for t in &fp.triangles {
for k in 0..3 {
let (a, b) = (t[k], t[(k + 1) % 3]);
*sides.entry((a.min(b), a.max(b))).or_default() += 1;
}
}
let boundary: f64 = sides.iter().filter(|(_, n)| **n == 1).map(|((a, b), _)| at(*a).dist(at(*b))).sum();
let sails = 2.0 / 3.0 * boundary * tol;
assert!(
(flat - surface).abs() <= 0.05 * surface + sails,
"{name}: face {} has {flat} mm² of triangles over {surface} mm² of surface (its boundary's sails {sails})",
fp.face
);
}
}
@@ -534,7 +553,7 @@ fn a_modelled_thread_in_a_bore_meshes_inside_its_block() {
let m = closed_display_mesh("thread in a bore", &body, tol);
let chord = max_chord(&body, &m);
assert!(chord <= tol * 1.05, "thread in a bore at {tol}: chord {chord}");
assert_faces_lie_flat("thread in a bore", &ok(body.mesh_plan(&crate::MeshTolerance { chord: tol, angle: Some(crate::DISPLAY_ANGLE) })));
assert_faces_lie_flat("thread in a bore", &ok(body.mesh_plan(&crate::MeshTolerance { chord: tol, angle: Some(crate::DISPLAY_ANGLE) })), tol);
}
}
@@ -560,7 +579,7 @@ fn a_thread_root_band_running_out_through_an_end_face_lies_on_its_surface() {
let chord = max_chord(&body, &m);
assert!(chord <= tol * 1.05, "thread run out at {tol}: chord {chord}");
assert!(m.triangles.len() < 60_000, "thread run out at {tol}: {} triangles", m.triangles.len());
assert_faces_lie_flat("thread run out", &ok(body.mesh_plan(&crate::MeshTolerance { chord: tol, angle: Some(crate::DISPLAY_ANGLE) })));
assert_faces_lie_flat("thread run out", &ok(body.mesh_plan(&crate::MeshTolerance { chord: tol, angle: Some(crate::DISPLAY_ANGLE) })), tol);
}
}