diff --git a/crates/kernel/src/tess/tests.rs b/crates/kernel/src/tess/tests.rs index 07f35669e5..9c35933c51 100644 --- a/crates/kernel/src/tess/tests.rs +++ b/crates/kernel/src/tess/tests.rs @@ -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); } }