mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 03:25:58 +00:00
kernel: Body::classify_point decides inside, outside or on the boundary exactly
SMLib's point classification at the body's own tolerance, on the exact faces and edges, in place of a mesh at a chord of 2e-3 of the body's size (about 0.4 mm on a 200 mm part). A point within the tolerance of a face, edge or vertex is OnBoundary, never decided by a ray. A mesh body's triangles are its exact shape: on them within the record's tolerance, else by ray crossings. The kernel's own probes move to it; Body::contains goes once its last callers have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKQ2rEZvcXNQnhE3Sgs
This commit is contained in:
@@ -404,8 +404,8 @@ mod tests {
|
||||
assert_eq!(result.unmeshed_faces(), 0);
|
||||
assert!(result.validity().is_empty(), "{:?}", result.validity());
|
||||
assert!((vol(&result) - vol(&reference)).abs() < 0.05);
|
||||
assert!(!result.contains(Vec3::new(24.9, 0.1, 5.0)));
|
||||
assert!(result.contains(Vec3::new(24.0, 1.0, 5.0)));
|
||||
assert_eq!(result.classify_point(Vec3::new(24.9, 0.1, 5.0)).unwrap(), crate::PointClass::Outside);
|
||||
assert_eq!(result.classify_point(Vec3::new(24.0, 1.0, 5.0)).unwrap(), crate::PointClass::Inside);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,8 +424,8 @@ mod tests {
|
||||
assert!(result.validity().is_empty(), "{:?}", result.validity());
|
||||
assert!((vol(&result) - vol(&reference)).abs() < 0.05, "radius {radius}");
|
||||
// The old bottom radius is still 2, away from the newly rounded corner.
|
||||
assert!(!result.contains(Vec3::new(12.5, 0.5, 0.5)));
|
||||
assert!(result.contains(Vec3::new(12.5, 1.0, 1.0)));
|
||||
assert_eq!(result.classify_point(Vec3::new(12.5, 0.5, 0.5)).unwrap(), crate::PointClass::Outside);
|
||||
assert_eq!(result.classify_point(Vec3::new(12.5, 1.0, 1.0)).unwrap(), crate::PointClass::Inside);
|
||||
}
|
||||
// A tangent boundary of an earlier round is not a sharp edge, and cannot be
|
||||
// reinterpreted as the sharp edge exposed when that round is taken off.
|
||||
@@ -470,8 +470,8 @@ mod tests {
|
||||
let result = fillet(&rounded, &crate::tangent_chain(&rounded, selection).unwrap(), 2.0).unwrap();
|
||||
assert!(result.validity().is_empty(), "{:?}", result.validity());
|
||||
assert!((vol(&result) - want).abs() < 1e-6 * want, "{selection:?}: {} vs {want}", vol(&result));
|
||||
assert!(!result.contains(Vec3::new(-4.9, 87.1, 10.0)));
|
||||
assert!(result.contains(Vec3::new(-4.1, 86.3, 10.0)));
|
||||
assert_eq!(result.classify_point(Vec3::new(-4.9, 87.1, 10.0)).unwrap(), crate::PointClass::Outside);
|
||||
assert_eq!(result.classify_point(Vec3::new(-4.1, 86.3, 10.0)).unwrap(), crate::PointClass::Inside);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +422,17 @@ pub struct Refusal {
|
||||
pub why: String,
|
||||
}
|
||||
|
||||
/// Where a point lies against a body ([`Body::classify_point`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum PointClass {
|
||||
/// In the body's material.
|
||||
Inside,
|
||||
/// Outside the material, or in a cavity.
|
||||
Outside,
|
||||
/// Within the body's tolerance of a face, an edge or a vertex.
|
||||
OnBoundary,
|
||||
}
|
||||
|
||||
/// The kind of surface a face lies on, as a person modelling would name it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SurfaceKind {
|
||||
@@ -1348,6 +1359,39 @@ impl Body {
|
||||
self.mesh(&MeshTolerance::chord(self.size() * 2e-3)).map(|m| m.contains(p)).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Where `p` lies against the body, decided exactly on its faces' surfaces and edges' curves
|
||||
/// (SMLib's point classification at the body's own tolerance, no mesh): on the boundary when
|
||||
/// within that tolerance of a face, an edge or a vertex, else inside its material or outside
|
||||
/// (a cavity is outside). A mesh body's triangles are its exact shape: on its boundary
|
||||
/// within the record's tolerance of a triangle, else inside when rays from the point cross
|
||||
/// it an odd number of times.
|
||||
pub fn classify_point(&self, p: Vec3) -> Result<PointClass> {
|
||||
if !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite()) {
|
||||
return Err(KernelError::Invalid(format!("point classification: the point {p:?} is not finite")));
|
||||
}
|
||||
if let Some(m) = &self.mesh {
|
||||
let mesh = m.to_mesh();
|
||||
let near = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
.filter_map(|t| mesh.tri(t))
|
||||
.any(|[a, b, c]| solidcraft_geom::compare::closest_on_triangle(p, a, b, c).dist(p) <= self.record.tolerance);
|
||||
return Ok(if near {
|
||||
PointClass::OnBoundary
|
||||
} else if mesh.contains(p) {
|
||||
PointClass::Inside
|
||||
} else {
|
||||
PointClass::Outside
|
||||
});
|
||||
}
|
||||
let class = self.smlib()?.classify_point([p.x, p.y, p.z]).map_err(|e| KernelError::Failed(format!("SMLib point classification: {e}")))?;
|
||||
Ok(match class {
|
||||
solidcraft_smlib::PointClass::Inside => PointClass::Inside,
|
||||
solidcraft_smlib::PointClass::Outside => PointClass::Outside,
|
||||
solidcraft_smlib::PointClass::OnBoundary => PointClass::OnBoundary,
|
||||
})
|
||||
}
|
||||
|
||||
/// B-rep faces (a mesh body is one face; [`crate::measure`] counts its triangles).
|
||||
pub fn face_count(&self) -> usize {
|
||||
if self.mesh.is_some() { 1 } else { self.record.faces.len() }
|
||||
|
||||
@@ -259,3 +259,52 @@ fn a_long_thin_cylinder_comes_out_outward() {
|
||||
assert!((volume - want).abs() < want * 0.02, "{lo} {seam}: {volume} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact point classification: a box's inside, outside, faces, edges and corners; a cylinder's
|
||||
/// curved wall within its tolerance (on it), 5 µm inside it (inside: the old mesh test, at a
|
||||
/// chord of 2e-3 of the size, missed this band) and just beyond; a cavity is outside.
|
||||
#[test]
|
||||
fn points_classify_exactly_against_the_body() {
|
||||
use crate::{PointClass::*, cylinder};
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
|
||||
for (p, want) in [
|
||||
(Vec3::new(5.0, 10.0, 15.0), Inside),
|
||||
(Vec3::new(5.0, 10.0, 31.0), Outside),
|
||||
(Vec3::new(5.0, 10.0, 30.0 + 1e-4), Outside),
|
||||
(Vec3::new(5.0, 10.0, 30.0 - 1e-4), Inside),
|
||||
(Vec3::new(5.0, 10.0, 30.0), OnBoundary),
|
||||
(Vec3::new(10.0, 10.0, 30.0), OnBoundary),
|
||||
(Vec3::new(10.0, 20.0, 30.0), OnBoundary),
|
||||
(Vec3::ZERO, OnBoundary),
|
||||
] {
|
||||
assert_eq!(b.classify_point(p).unwrap(), want, "{p:?}");
|
||||
}
|
||||
assert!(b.classify_point(Vec3::new(f64::NAN, 0.0, 0.0)).is_err());
|
||||
|
||||
let c = cylinder(Vec3::ZERO, Vec3::Z, 5.0, 10.0).unwrap();
|
||||
let tol = c.record().tolerance;
|
||||
let at = |r: f64| Vec3::new(r * std::f64::consts::FRAC_1_SQRT_2, r * std::f64::consts::FRAC_1_SQRT_2, 5.0);
|
||||
assert_eq!(c.classify_point(at(5.0 + 0.5 * tol)).unwrap(), OnBoundary);
|
||||
assert_eq!(c.classify_point(at(5.0 - 0.5 * tol)).unwrap(), OnBoundary);
|
||||
assert_eq!(c.classify_point(at(5.0 - 5e-3)).unwrap(), Inside);
|
||||
assert_eq!(c.classify_point(at(5.0 + 1e-4)).unwrap(), Outside);
|
||||
assert_eq!(c.classify_point(Vec3::new(0.0, 0.0, 10.0)).unwrap(), OnBoundary);
|
||||
|
||||
let hollow = boolean(&cube(50.0), &cube(40.0), BoolOp::Cut).unwrap().unwrap();
|
||||
assert_eq!(hollow.classify_point(Vec3::ZERO).unwrap(), Outside, "the cavity");
|
||||
assert_eq!(hollow.classify_point(Vec3::X * 45.0).unwrap(), Inside);
|
||||
assert_eq!(hollow.classify_point(Vec3::X * 40.0).unwrap(), OnBoundary, "the cavity's wall");
|
||||
}
|
||||
|
||||
/// A mesh body's triangles are its shape: on them within the record's tolerance, else inside or
|
||||
/// outside by the rays' crossings.
|
||||
#[test]
|
||||
fn points_classify_against_a_mesh_body() {
|
||||
use crate::PointClass::*;
|
||||
let p = [Vec3::ZERO, Vec3::X * 10.0, Vec3::Y * 10.0, Vec3::Z * 10.0];
|
||||
let m = crate::mesh_body(&p, &[[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]]).unwrap();
|
||||
assert_eq!(m.classify_point(Vec3::new(1.0, 1.0, 1.0)).unwrap(), Inside);
|
||||
assert_eq!(m.classify_point(Vec3::new(5.0, 5.0, 5.0)).unwrap(), Outside);
|
||||
assert_eq!(m.classify_point(Vec3::new(1.0, 1.0, 0.0)).unwrap(), OnBoundary);
|
||||
assert_eq!(m.classify_point(Vec3::X * 10.0).unwrap(), OnBoundary);
|
||||
}
|
||||
|
||||
@@ -358,7 +358,7 @@ mod tests {
|
||||
assert_eq!(restored.face_count(), 6);
|
||||
assert_eq!(restored.unmeshed_faces(), 0);
|
||||
assert!((volume(&restored) - 6250.0).abs() < 1e-6);
|
||||
assert!(restored.contains(Vec3::new(0.1, 0.1, 0.1)));
|
||||
assert_eq!(restored.classify_point(Vec3::new(0.1, 0.1, 0.1)).unwrap(), crate::PointClass::Inside);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -162,7 +162,8 @@ pub use blend::{
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
|
||||
pub use body::{
|
||||
Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, MeshTolerance, Paint, Refusal, SurfaceKind,
|
||||
Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, MeshTolerance, Paint, PointClass, Refusal,
|
||||
SurfaceKind,
|
||||
};
|
||||
pub use boundary::{Boundary, boundary_patch};
|
||||
pub use boundary_network::{BoundaryCurve, CurveSource, MAX_CHAIN_CURVES, MAX_EDGE_PICKS, boundary_join, boundary_surface};
|
||||
|
||||
@@ -48,8 +48,8 @@ fn projected_spherical_outline_does_not_block_a_lens_cut() {
|
||||
let measured = crate::measure(&result).unwrap();
|
||||
assert!((measured.volume - 8810.08973246).abs() < 2., "{measured:?}");
|
||||
assert!(measured.bbox.max.z < 20.01, "no thin lid may remain above the cut: {measured:?}");
|
||||
assert!(result.contains(Vec3::new(0., -10., 2.)));
|
||||
assert!(!result.contains(Vec3::new(0., -10., 15.)));
|
||||
assert_eq!(result.classify_point(Vec3::new(0., -10., 2.)).unwrap(), crate::PointClass::Inside);
|
||||
assert_eq!(result.classify_point(Vec3::new(0., -10., 15.)).unwrap(), crate::PointClass::Outside);
|
||||
assert_eq!(measured.merged.face_types.get("cylinder"), Some(&1));
|
||||
let history = result.history().expect("the cut has a face history");
|
||||
history.check(&result, &[&target, &cutter]).unwrap();
|
||||
|
||||
@@ -73,9 +73,9 @@ fn chamfer_the_top_lip_at_a_tangent_wall_junction() {
|
||||
let expected = expected_removed(2.0);
|
||||
eprintln!("removed {removed:.8} mm³, expected {expected:.8} mm³");
|
||||
assert!((removed - expected).abs() < 0.03, "removed {removed}, expected {expected}");
|
||||
assert!(!made.contains(Vec3::new(10.0, 9.5, 59.5)), "the old corner was removed");
|
||||
assert!(made.contains(Vec3::new(10.0, 7.0, 59.5)), "material behind the bevel remains");
|
||||
assert!(made.contains(Vec3::new(10.0, 10.0, 55.0)), "material below the bevel remains");
|
||||
assert_eq!(made.classify_point(Vec3::new(10.0, 9.5, 59.5)).unwrap(), solidcraft_kernel::PointClass::Outside, "the old corner was removed");
|
||||
assert_eq!(made.classify_point(Vec3::new(10.0, 7.0, 59.5)).unwrap(), solidcraft_kernel::PointClass::Inside, "material behind the bevel remains");
|
||||
assert_eq!(made.classify_point(Vec3::new(10.0, 10.0, 55.0)).unwrap(), solidcraft_kernel::PointClass::Inside, "material below the bevel remains");
|
||||
// A closed mesh's signed volume is independent of the origin.
|
||||
let volume_about = |origin: Vec3| {
|
||||
after
|
||||
|
||||
@@ -54,7 +54,10 @@ fn shell_of_a_revolved_barrel() {
|
||||
assert!((volume - full).abs() < full * 1e-3, "{volume} vs {full}");
|
||||
for turn in [0.0, 1.0, 2.5, 4.0] {
|
||||
let (c, s) = (f64::cos(turn), f64::sin(turn));
|
||||
assert!(barrel.contains(Vec3::new(13.8 * c, 13.8 * s, HALF_HEIGHT)) && !barrel.contains(Vec3::new(14.2 * c, 14.2 * s, HALF_HEIGHT)));
|
||||
assert!(
|
||||
barrel.classify_point(Vec3::new(13.8 * c, 13.8 * s, HALF_HEIGHT)).unwrap() == solidcraft_kernel::PointClass::Inside
|
||||
&& barrel.classify_point(Vec3::new(14.2 * c, 14.2 * s, HALF_HEIGHT)).unwrap() == solidcraft_kernel::PointClass::Outside
|
||||
);
|
||||
}
|
||||
let result = shell(&barrel, &[Vec3::new(0.0, 0.0, 2.0 * HALF_HEIGHT)], 2.0).unwrap();
|
||||
assert!(result.validity().is_empty(), "{:?}", result.validity());
|
||||
|
||||
Reference in New Issue
Block a user