1 parent
1ded8fdd27
commit
2c020dbcbc
85 files changed
+43
-14487
No files matched your search
@@ -1612,9 +1612,3 @@ mod display_mesh_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "regularization_protection_tests.rs"]
|
||||
mod regularization_protection_tests;
|
||||
|
||||
#[path = "body_verification.rs"]
|
||||
mod verification;
|
||||
#[cfg(test)]
|
||||
#[path = "body_verification_tests.rs"]
|
||||
mod verification_tests;
|
||||
@@ -1,58 +0,0 @@
|
||||
//! Manufacturing mesh path: no display LOD, tolerance floor or missing-face fallback.
|
||||
use super::*;
|
||||
|
||||
impl Body {
|
||||
/// Mesh at a fixed physical chord tolerance. Refinement/output exhaustion is an error.
|
||||
pub fn tessellate_for_verification(&self, tol: f64, triangle_limit: usize) -> Result<Mesh> {
|
||||
if !tol.is_finite() || tol <= 0. || triangle_limit == 0 || triangle_limit > 2_000_000 {
|
||||
return Err(KernelError::Invalid("verification tessellation needs positive finite tolerance and 1–2000000 triangles".into()));
|
||||
}
|
||||
if let Some(mesh) = &self.mesh {
|
||||
if mesh.triangles.len() > triangle_limit {
|
||||
return Err(KernelError::Failed("verification tessellation exceeds triangle budget".into()));
|
||||
}
|
||||
return Ok(mesh.to_mesh());
|
||||
}
|
||||
guard("verification tessellation", || {
|
||||
let mut out = Mesh::default();
|
||||
let mut face_index = 0_u32;
|
||||
for shell in self.solid.boundaries() {
|
||||
let mesh = truck_meshalgo::tessellation::robust_triangulation_checked(shell, tol)
|
||||
.ok_or_else(|| KernelError::Failed("verification tessellation cannot meet tolerance within refinement budget".into()))?;
|
||||
if mesh.face_iter().count() != shell.face_iter().count() {
|
||||
return Err(KernelError::Failed("verification tessellation has missing faces".into()));
|
||||
}
|
||||
for face in mesh.face_iter() {
|
||||
let pm =
|
||||
face.surface().ok_or_else(|| KernelError::Failed(format!("verification tessellation could not mesh face {face_index}")))?;
|
||||
let count = pm.faces().triangle_iter().count();
|
||||
if count == 0 || count > triangle_limit.saturating_sub(out.triangles.len()) {
|
||||
return Err(KernelError::Failed("verification tessellation has empty face or exceeds triangle budget".into()));
|
||||
}
|
||||
let flip = !face.orientation();
|
||||
for tri in pm.faces().triangle_iter() {
|
||||
let mut indices = [0; 3];
|
||||
for (slot, vertex) in indices.iter_mut().zip(tri) {
|
||||
let p = pm.positions().get(vertex.pos).ok_or_else(|| KernelError::Failed("invalid verification vertex".into()))?;
|
||||
let p = Vec3::new(p.x, p.y, p.z);
|
||||
if ![p.x, p.y, p.z].into_iter().all(f64::is_finite) {
|
||||
return Err(KernelError::Failed("non-finite verification vertex".into()));
|
||||
}
|
||||
*slot = u32::try_from(out.positions.len()).map_err(|_| KernelError::Failed("verification vertex limit".into()))?;
|
||||
out.positions.push(p);
|
||||
let n = vertex.nor.and_then(|i| pm.normals().get(i)).map(|n| Vec3::new(n.x, n.y, n.z)).unwrap_or(Vec3::Z);
|
||||
out.normals.push(if flip { -n } else { n });
|
||||
}
|
||||
out.triangles.push(if flip { [indices[0], indices[2], indices[1]] } else { indices });
|
||||
out.tri_face.push(face_index);
|
||||
}
|
||||
face_index += 1;
|
||||
}
|
||||
}
|
||||
if face_index == 0 {
|
||||
return Err(KernelError::Failed("verification tessellation has no faces".into()));
|
||||
}
|
||||
Ok(out)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
//! Independent analytic checks for the fixed-tolerance verification mesh.
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
// Run the original candidate-index differential tests in ordinary workspace CI;
|
||||
// the vendored crate's standalone test targets are intentionally disabled.
|
||||
#[path = "../../../vendor/truck-meshalgo/src/tessellation/boundary_index.rs"]
|
||||
mod boundary_index;
|
||||
|
||||
fn radial_error(mesh: &Mesh, radius: f64, center: Vec3) -> f64 {
|
||||
let mut error = 0_f64;
|
||||
for indices in &mesh.triangles {
|
||||
let points = mesh.tri(indices).unwrap();
|
||||
// The cylinder side spans both cap heights. Check every side edge,
|
||||
// including triangle diagonals, against the analytic radial distance.
|
||||
if points.iter().any(|p| p.z < center.z + 1.) && points.iter().any(|p| p.z > center.z + 5.) {
|
||||
for i in 0..3 {
|
||||
let midpoint = (points[i] + points[(i + 1) % 3]) * 0.5 - center;
|
||||
error = error.max((radius - midpoint.x.hypot(midpoint.y)).abs());
|
||||
}
|
||||
}
|
||||
}
|
||||
error
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_tessellation_keeps_physical_tolerance_for_large_curved_bodies() {
|
||||
let tolerance = 0.00125;
|
||||
let radius = 5000.;
|
||||
let center = Vec3::new(20_000., -10_000., 3.);
|
||||
let body = crate::cylinder(center, Vec3::Z, radius, 6.).unwrap();
|
||||
let display = body.tessellate(tolerance).unwrap();
|
||||
assert!(radial_error(&display, radius, center) > tolerance * 4.);
|
||||
let mesh = body.tessellate_for_verification(tolerance, 200_000).unwrap();
|
||||
assert!(mesh.triangles.len() > display.triangles.len());
|
||||
let volume = mesh.measure().volume;
|
||||
assert!(volume >= std::f64::consts::PI * (radius - tolerance).powi(2) * 6.);
|
||||
assert!(volume <= std::f64::consts::PI * (radius + tolerance).powi(2) * 6.);
|
||||
assert!(radial_error(&mesh, radius, center) <= tolerance, "{}", radial_error(&mesh, radius, center));
|
||||
assert_eq!(mesh.tri_face.iter().copied().collect::<std::collections::BTreeSet<_>>().len(), body.face_count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_tessellation_rejects_bad_settings_and_excessive_output() {
|
||||
let body = crate::box_solid(Vec3::ZERO, Vec3::new(10., 10., 6.)).unwrap();
|
||||
for tolerance in [0., -1., f64::NAN, f64::INFINITY] {
|
||||
assert!(body.tessellate_for_verification(tolerance, 100).is_err());
|
||||
}
|
||||
for limit in [0, 2_000_001] {
|
||||
assert!(body.tessellate_for_verification(0.00125, limit).is_err());
|
||||
}
|
||||
assert!(body.tessellate_for_verification(0.00125, 11).unwrap_err().to_string().contains("triangle budget"));
|
||||
let mesh = body.tessellate_for_verification(0.00125, 12).unwrap();
|
||||
assert_eq!(mesh.triangles.len(), 12);
|
||||
let imported = crate::mesh_body(&mesh.positions, &mesh.triangles).unwrap();
|
||||
assert!(imported.tessellate_for_verification(0.00125, 11).is_err());
|
||||
assert_eq!(imported.tessellate_for_verification(0.00125, 12).unwrap().triangles.len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_tessellation_does_not_accept_refinement_budget_fallback() {
|
||||
let body = crate::cylinder(Vec3::ZERO, Vec3::Z, 5000., 6.).unwrap();
|
||||
let error = body.tessellate_for_verification(1e-10, 200_000).unwrap_err();
|
||||
assert!(error.to_string().contains("refinement budget"), "{error}");
|
||||
// Curved in both parameters: the surface grid exhausts its budget before
|
||||
// any circular boundary needs the edge's 20,000-point limit.
|
||||
let sphere = crate::sphere(Vec3::ZERO, 10.).unwrap();
|
||||
let error = sphere.tessellate_for_verification(1e-5, 200_000).unwrap_err();
|
||||
assert!(error.to_string().contains("refinement budget"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_tessellation_preserves_concave_material_and_multiple_holes() {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
|
||||
let polygon = |points: &[(f64, f64)]| Loop2::polygon(&points.iter().map(|&(x, y)| Vec2::new(x, y)).collect::<Vec<_>>());
|
||||
let region = Region2 {
|
||||
outer: polygon(&[(0., 0.), (20., 0.), (20., 10.), (10., 10.), (10., 20.), (0., 20.)]),
|
||||
holes: vec![polygon(&[(2., 2.), (2., 4.), (4., 4.), (4., 2.)]), polygon(&[(12., 2.), (12., 4.), (14., 4.), (14., 2.)])],
|
||||
};
|
||||
let bodies = crate::extrude(&Plane::XY, &[region], 0., 6.).unwrap();
|
||||
assert_eq!(bodies.len(), 1);
|
||||
let mesh = bodies.first().unwrap().tessellate_for_verification(0.00125, 200_000).unwrap();
|
||||
assert!((mesh.measure().volume - 1752.).abs() < 1e-6);
|
||||
for (point, material) in [
|
||||
(Vec3::new(1., 1., 3.), true),
|
||||
(Vec3::new(19., 9., 3.), true),
|
||||
(Vec3::new(9., 19., 3.), true),
|
||||
(Vec3::new(3., 3., 3.), false),
|
||||
(Vec3::new(13., 3., 3.), false),
|
||||
(Vec3::new(15., 15., 3.), false),
|
||||
] {
|
||||
assert_eq!(mesh.contains(point), material, "{point:?}");
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@ pub use step_in::{
|
||||
};
|
||||
pub use step_out::{ExportBody, ExportProduct, StepHeader, step_export_bodies, step_export_products};
|
||||
pub use surface_extend::{ExtendLimit, ExtendMode, ExtendOptions, extend_surface};
|
||||
pub use surface_kind::{EdgeCircle, FaceSurface, edge_circle, face_surface, hole_axis};
|
||||
pub use surface_kind::{EdgeCircle, FaceSurface, edge_circle, face_surface};
|
||||
pub use surface_offset::offset_surface;
|
||||
pub use surface_ruled::{Ruled, RuledCorners, RuledMode, ruled_surface};
|
||||
pub use surface_untrim::{UntrimHoles, untrim};
|
||||
|
||||
@@ -165,21 +165,6 @@ impl FaceSurface {
|
||||
}
|
||||
}
|
||||
|
||||
/// An inward cylindrical face's axis and radius. The caller still verifies the
|
||||
/// machined result against the complete target, including trimmed face bounds.
|
||||
pub fn hole_axis(b: &Body, face: usize, tolerance: f64) -> crate::Result<(Vec3, Vec3, f64)> {
|
||||
if !tolerance.is_finite() || tolerance <= 0. {
|
||||
return Err(crate::KernelError::Invalid("invalid hole tolerance".into()));
|
||||
}
|
||||
crate::guard("hole face axis", || {
|
||||
let f = b.solid.face_iter().nth(face).ok_or_else(|| crate::KernelError::Invalid("hole face no longer exists".into()))?;
|
||||
match surf_of(f, tolerance) {
|
||||
Some(Surf::Cylinder { o, a, r, convex: false }) => Ok((o, a, r)),
|
||||
_ => Err(crate::KernelError::Invalid("select an inward cylindrical hole face".into())),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user