mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
WIP kernel: Body::mesh(&MeshTolerance) as the one mesher (does not build yet)
Body::mesh / mesh_plan / mesh_of and MeshTolerance (chord, display, display_of, export_of) replace tessellate, tessellate_with, display_mesh*, display_plan*, reference_display_mesh_angular; every caller in kernel, doc, engine, io, ui-egui, gpu and the CLI renamed mechanically; the truck-mesher fallback, mesh_shells*, refined* removed; Body::new orients from the planner; truck-meshalgo dropped from Cargo.toml with the tests of its hooks. Still to fix: trait imports in tess/*, splitface/lumps/preview/step_in/boolean_record meshalgo uses, measure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
Generated
-1
@@ -4370,7 +4370,6 @@ dependencies = [
|
||||
"solidcraft-step",
|
||||
"spade",
|
||||
"thiserror 2.0.21",
|
||||
"truck-meshalgo",
|
||||
"truck-modeling",
|
||||
"truck-shapeops",
|
||||
"truck-stepio",
|
||||
|
||||
@@ -50,7 +50,6 @@ semver = "1"
|
||||
# geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it.
|
||||
truck-modeling = "0.6"
|
||||
truck-shapeops = { version = "0.4", default-features = false }
|
||||
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation", "analyzers"] }
|
||||
truck-stepio = { version = "0.3", default-features = false }
|
||||
truck-topology = "0.6"
|
||||
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
|
||||
|
||||
@@ -238,7 +238,7 @@ fn check_case(dir: &str) -> Value {
|
||||
shells += m.shells as u64;
|
||||
if want["watertight"].as_bool() == Some(true) {
|
||||
let tol = want["tolerance"]["mesh_mm"].as_f64().unwrap_or(MESH_TOL);
|
||||
let (open, over) = match b.body.tessellate(tol) {
|
||||
let (open, over) = match b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol)) {
|
||||
Ok(mesh) => open_edges(&mesh.positions, &mesh.triangles, want["tolerance"]["weld_mm"].as_f64().unwrap_or(1e-4)),
|
||||
Err(e) => {
|
||||
c.push("watertight", json!(format!("{}: {e}", b.name)), json!("no open edges"), false);
|
||||
|
||||
@@ -187,7 +187,7 @@ fn step_export_check(s: &solidcraft_engine::Session, volume: f64) -> (bool, Stri
|
||||
fn our_mesh(s: &solidcraft_engine::Session) -> Result<Vec<Tri>, String> {
|
||||
let mut ours: Vec<Tri> = Vec::new();
|
||||
for b in &s.world_state().bodies {
|
||||
let m = b.body.tessellate_with(MESH_TOL, true).map_err(|e| e.to_string())?;
|
||||
let m = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(MESH_TOL)).map_err(|e| e.to_string())?;
|
||||
let p = |i: u32| m.positions.get(i as usize).map(|v: &Vec3| [v.x, v.y, v.z]);
|
||||
for t in &m.triangles {
|
||||
if let (Some(a), Some(b), Some(c)) = (p(t[0]), p(t[1]), p(t[2])) {
|
||||
|
||||
@@ -1338,7 +1338,7 @@ fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
|
||||
fn a_round_on_a_knuckle_cap_circle() {
|
||||
for (cmd, size, removed) in [("solid.fillet", "radius", (2.4, 2.8)), ("solid.chamfer", "distance", (5.6, 6.3))] {
|
||||
let mut s = load(&root().join("tests/oracle/63-hinge-knuckle/recipe.json")).unwrap();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate(0.001).unwrap().measure().volume).sum::<f64>();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute(cmd, &json!({"edges": [[40.0, 29.837_722, 6.449_490]], size: 0.728})).unwrap_or_else(|e| panic!("{cmd}: {e}"));
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{cmd}: {kind}: {e}"));
|
||||
@@ -1358,7 +1358,7 @@ fn a_round_on_a_knuckle_cap_circle() {
|
||||
#[test]
|
||||
fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
|
||||
let mut s = load(&root().join("examples/regressions/loft_cut_beside_round_hole.json")).unwrap();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.005, true).unwrap().measure().volume).sum::<f64>();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 0.133, "position": [357.22, -150.993_333, 730.0]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1375,7 +1375,7 @@ fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
|
||||
#[test]
|
||||
fn a_small_hole_through_the_wall_of_a_plastic_rest() {
|
||||
let mut s = load(&root().join("apps/solidcraft-cli/tests/corpus/modern-plastic.solidcraft")).unwrap();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 0.8, "position": [63.069_841_5, 28.333_333, 4.0]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1390,7 +1390,7 @@ fn a_small_hole_through_the_wall_of_a_plastic_rest() {
|
||||
#[test]
|
||||
fn a_hole_square_to_a_45_degree_flange() {
|
||||
let mut s = load(&root().join("tests/oracle/70-sm-flange-45/recipe.json")).unwrap();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>();
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 3.448, "position": [66.666_667, -4.511_845, 4.511_845]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1728,7 +1728,7 @@ fn the_ribbed_lid_builds_the_same_body_every_time() {
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let mesh = b.body.tessellate(0.1).unwrap();
|
||||
let mesh = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.1)).unwrap();
|
||||
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
|
||||
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
|
||||
});
|
||||
|
||||
@@ -26,8 +26,8 @@ fn assert_two_through_holes(session: &Session, stage: &str) {
|
||||
let expected = 2.0 * PI * 4.0_f64.powi(2) * 6.0;
|
||||
assert!((removed - expected).abs() < expected * 1e-3, "{stage}: the two holes remove {removed} mm³, expected {expected} mm³");
|
||||
|
||||
let before_mesh = uncut.tessellate(0.01).unwrap();
|
||||
let after_mesh = bracket.tessellate(0.01).unwrap();
|
||||
let before_mesh = uncut.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let after_mesh = bracket.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let down = Vec3::new(0.0, 0.0, -1.0);
|
||||
for y in [-15.0, -45.0] {
|
||||
let center = Vec3::new(25.0, y, 1.0);
|
||||
|
||||
@@ -180,7 +180,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
let Some(body) = state.bodies.iter().find(|b| b.definition_name() == definition && placed.model_body_component(b) == component) else {
|
||||
continue;
|
||||
};
|
||||
let mesh = body.body.tessellate(0.01).unwrap();
|
||||
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
for index in 0..body.body.face_count() {
|
||||
if body.body.planar_face(index).is_none() {
|
||||
continue;
|
||||
|
||||
@@ -52,7 +52,7 @@ fn straight_edges_are_one_edge_after_many_cuts() {
|
||||
let bodies: Vec<_> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
|
||||
assert_eq!(bodies.len(), 1, "{what}: one body");
|
||||
let body = &bodies[0].body;
|
||||
let mesh = body.tessellate(0.01).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
// Every edge is drawn as one polyline; a straight one as one segment.
|
||||
let mut at: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
|
||||
for (i, e) in mesh.edges.iter().enumerate() {
|
||||
|
||||
@@ -187,7 +187,7 @@ mod tests {
|
||||
}
|
||||
assert!(std::sync::Arc::ptr_eq(&reference, &mb.mesh()));
|
||||
assert_eq!(mb.display_plan_at(DisplayQuality::Normal).unwrap().vertex_count(), {
|
||||
let plan = mb.body.display_plan_angular((mb.body.size() * 1e-3).max(1e-3), solidcraft_kernel::DISPLAY_ANGLE).unwrap();
|
||||
let plan = mb.body.mesh_plan(&solidcraft_kernel::MeshTolerance { chord: (mb.body.size() * 1e-3).max(1e-3), angle: Some(solidcraft_kernel::DISPLAY_ANGLE) }).unwrap();
|
||||
plan.vertex_count()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ impl ModelBody {
|
||||
.get(q.index())?
|
||||
.get_or_init(|| match self.mesh_placement.as_deref() {
|
||||
Some(placed) => placed.plan_mesh(q, &self.name),
|
||||
None => self.display_plan_at(q).and_then(|plan| self.body.display_mesh_of(&plan).ok()).map(Arc::new),
|
||||
None => self.display_plan_at(q).and_then(|plan| self.body.mesh_of(&plan).ok()).map(Arc::new),
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
@@ -301,14 +301,14 @@ impl ModelBody {
|
||||
if k == 0 {
|
||||
return Some((self.display_plan_at(q)?, self.plan_mesh_at(q)?));
|
||||
}
|
||||
let plan = match self.body.display_plan_angular(q.refined_chord(self.body.size(), k), q.refined_angle(k)) {
|
||||
let plan = match self.body.mesh_plan(&solidcraft_kernel::MeshTolerance { chord: q.refined_chord(self.body.size(), k), angle: Some(q.refined_angle(k)) }) {
|
||||
Ok(plan) => plan,
|
||||
Err(e) => {
|
||||
log::debug!("{}: no display plan refined {k} steps ({e})", self.name);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mesh = self.body.display_mesh_of(&plan).ok()?;
|
||||
let mesh = self.body.mesh_of(&plan).ok()?;
|
||||
Some((Arc::new(plan), Arc::new(mesh)))
|
||||
}
|
||||
|
||||
@@ -3832,7 +3832,7 @@ fn resolve_edges(b: &Body, pts: &[Vec3], refs: &[EdgeRef], warning: &mut Option<
|
||||
let edges = b.edges((size * 1e-3).max(1e-3))?;
|
||||
let tight = (size * 2e-3).max(1e-3);
|
||||
let dist = |p: Vec3, e: &kernel::EdgeInfo| e.points.windows(2).map(|w| p.dist_to_segment(w[0], w[1])).fold(f64::INFINITY, f64::min);
|
||||
let now = b.tessellate((size * 1e-2).max(1e-2)).map(|m| m.bounds()).ok();
|
||||
let now = b.mesh(&solidcraft_kernel::MeshTolerance::chord((size * 1e-2).max(1e-2))).map(|m| m.bounds()).ok();
|
||||
let (mut out, mut moved, mut lost) = (Vec::new(), 0, 0);
|
||||
for (i, p) in pts.iter().enumerate() {
|
||||
let on = edges.iter().any(|e| dist(*p, e) <= tight);
|
||||
@@ -6796,7 +6796,7 @@ fn trim_to_body(tools: Vec<Traced>, target: &ModelBody, dist_to_start: impl Fn(V
|
||||
for (lump, lineage) in lumps.into_iter().zip(lineages) {
|
||||
let tol = (lump.size() * 1e-4).max(1e-6);
|
||||
let touches =
|
||||
lump.tessellate((lump.size() * 1e-2).max(1e-2)).map(|m| m.positions.iter().any(|q| dist_to_start(*q) < tol)).unwrap_or(false);
|
||||
lump.mesh(&solidcraft_kernel::MeshTolerance::chord((lump.size() * 1e-2).max(1e-2))).map(|m| m.positions.iter().any(|q| dist_to_start(*q) < tol)).unwrap_or(false);
|
||||
if touches {
|
||||
out.push(Traced { body: lump, lineage });
|
||||
}
|
||||
@@ -6975,7 +6975,7 @@ fn face_plane(st: &ModelState, picked: &Plane, at: Vec3, name: Option<&str>) ->
|
||||
let mut best: Option<(f64, f64)> = None; // (distance from `at`, plane offset along n)
|
||||
for b in &st.bodies {
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
let (Ok(faces), Ok(mesh)) = (b.body.faces(tol), b.body.tessellate(tol)) else { continue };
|
||||
let (Ok(faces), Ok(mesh)) = (b.body.faces(tol), b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol))) else { continue };
|
||||
for f in faces {
|
||||
let Some(fnrm) = f.plane_normal else { continue };
|
||||
if fnrm.dot(n) < 1.0 - 1e-6 {
|
||||
|
||||
@@ -198,7 +198,7 @@ pub(super) fn rib_tools(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Fea
|
||||
fn planar_normal_at(b: &Body, p: Vec3) -> Option<Vec3> {
|
||||
let tol = (b.size() * 2e-3).max(1e-3);
|
||||
let faces = b.faces(tol).ok()?;
|
||||
let mesh = b.tessellate(tol).ok()?;
|
||||
let mesh = b.mesh(&solidcraft_kernel::MeshTolerance::chord(tol)).ok()?;
|
||||
let mut near = vec![f64::INFINITY; faces.len()];
|
||||
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
if let (Some([u, v, w]), Some(d)) = (mesh.tri(t), near.get_mut(*f as usize)) {
|
||||
@@ -807,11 +807,11 @@ pub(super) fn sheet_eval(
|
||||
// thin plate that thousandth passes the thickness: the far side's triangles counted as
|
||||
// this face's (no outline was left), and a hole of a smaller radius than that fell out.
|
||||
let mut tol = (mb.body.size() * 1e-3).max(1e-3);
|
||||
let mut mesh = mb.body.tessellate(tol)?;
|
||||
let mut mesh = mb.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol))?;
|
||||
let (mut lo, mut hi) = extent(&mesh);
|
||||
if hi - lo > 0.0 && tol > (hi - lo) * 0.1 {
|
||||
tol = ((hi - lo) * 0.1).max(1e-6);
|
||||
mesh = mb.body.tessellate(tol)?;
|
||||
mesh = mb.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol))?;
|
||||
(lo, hi) = extent(&mesh);
|
||||
}
|
||||
let t = hi - lo;
|
||||
@@ -1536,7 +1536,7 @@ fn sheet_prism_cut(
|
||||
/// more): bodies whose boxes are apart do not meet. None when it can't be worked out.
|
||||
fn body_box(body: &Body) -> Option<Aabb3> {
|
||||
let tol = (body.size() * 1e-3).max(1e-4);
|
||||
padded_box(&body.tessellate(tol).ok()?, tol)
|
||||
padded_box(&body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol)).ok()?, tol)
|
||||
}
|
||||
|
||||
/// [`body_box`] of a model body, from the mesh it keeps.
|
||||
@@ -1814,7 +1814,7 @@ fn plate_recesses(body: &Body, mesh: &Mesh, plane: &Plane, at: Vec3, n: Vec3, (l
|
||||
)));
|
||||
}
|
||||
// The floors' outlines, from a mesh fine enough to mark them by.
|
||||
let fine = body.tessellate((t * 1e-2).min(tol).max(1e-4))?;
|
||||
let fine = body.mesh(&solidcraft_kernel::MeshTolerance::chord((t * 1e-2).min(tol).max(1e-4)))?;
|
||||
floors.sort_by(|a, b| depth_of(a.1).total_cmp(&depth_of(b.1)));
|
||||
// Floors at one depth are one pocket where they meet.
|
||||
let mut groups: Vec<(f64, Vec<Region2>)> = Vec::new();
|
||||
@@ -2032,7 +2032,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
|
||||
}
|
||||
let plane = Plane::from_normal(face.0, n).ok_or_else(|| DocError::Invalid("rim plane".into()))?;
|
||||
let tol = (mb.body.size() * 1e-3).max(1e-3);
|
||||
let mesh = mb.body.tessellate(tol)?;
|
||||
let mesh = mb.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol))?;
|
||||
let mut loops = face_loops(&mesh, &plane, face.0, n, 0.0, tol);
|
||||
loops.sort_by(|a, b| b.signed_area().abs().total_cmp(&a.signed_area().abs()));
|
||||
let ccw = |l: &Ring<Vec2>| if l.signed_area() < 0.0 { l.iter().rev().copied().collect::<Ring<_>>() } else { l.clone() };
|
||||
|
||||
@@ -323,7 +323,7 @@ fn profile_points(doc: &Document, st: &ModelState, p: &CamProfile, cam: &super::
|
||||
// Each body sampled to its own size (a large part beside the cam leaves the cam fine).
|
||||
// Each body's edges with the faces beside them.
|
||||
let meshes: Vec<solidcraft_geom::Mesh> =
|
||||
bodies.iter().map(|b| b.body.tessellate(tolerance(b.body.size()))).collect::<std::result::Result<_, _>>()?;
|
||||
bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tolerance(b.body.size())))).collect::<std::result::Result<_, _>>()?;
|
||||
let edges: Vec<(usize, &Vec<Vec3>, &[u32])> = meshes
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
||||
@@ -75,7 +75,7 @@ impl PlacedMesh {
|
||||
let source = cell
|
||||
.get_or_init(|| {
|
||||
let plan = definition_plan(&self.body, self.plans.get(q.index())?, q, name)?;
|
||||
self.body.display_mesh_of(&plan).ok().map(Arc::new)
|
||||
self.body.mesh_of(&plan).ok().map(Arc::new)
|
||||
})
|
||||
.clone()?;
|
||||
Some((source, self.transform))
|
||||
@@ -105,7 +105,7 @@ pub(super) fn definition_plan(
|
||||
q: crate::display_quality::DisplayQuality,
|
||||
name: &str,
|
||||
) -> Option<Arc<TessPlan>> {
|
||||
cell.get_or_init(|| match body.display_plan_angular(q.chord(body.size()), q.angle()) {
|
||||
cell.get_or_init(|| match body.mesh_plan(&solidcraft_kernel::MeshTolerance { chord: q.chord(body.size()), angle: Some(q.angle()) }) {
|
||||
Ok(plan) => Some(Arc::new(plan)),
|
||||
Err(e) => {
|
||||
log::debug!("{name}: no display plan ({e})");
|
||||
@@ -117,7 +117,7 @@ pub(super) fn definition_plan(
|
||||
|
||||
pub(super) fn reference_mesh(body: &Body, mesh: &OnceLock<Arc<Mesh>>, tolerance: f64) -> Arc<Mesh> {
|
||||
mesh.get_or_init(|| {
|
||||
let mut mesh = body.reference_display_mesh_angular(tolerance, kernel::DISPLAY_ANGLE).unwrap_or_default();
|
||||
let mut mesh = body.mesh(&solidcraft_kernel::MeshTolerance { chord: tolerance, angle: Some(kernel::DISPLAY_ANGLE) }).unwrap_or_default();
|
||||
if let Some((positions, triangles)) = body.triangle_mesh() {
|
||||
mesh.edges = mesh_outlines(positions, triangles);
|
||||
mesh.display_only_edges = true;
|
||||
|
||||
@@ -1691,7 +1691,7 @@ impl SheetBody {
|
||||
for longer in [false, true] {
|
||||
let tool = if longer {
|
||||
let axis = bore.axis.normalized().ok_or_else(|| DocError::Invalid("invalid sheet bore".into()))?;
|
||||
let mesh = inside.tessellate((bore.radius * 0.1).max(1e-4))?;
|
||||
let mesh = inside.mesh(&solidcraft_kernel::MeshTolerance::chord((bore.radius * 0.1).max(1e-4)))?;
|
||||
let (lo, hi) =
|
||||
mesh.positions.iter().map(|p| (*p - bore.base).dot(axis)).fold((f64::MAX, f64::MIN), |(lo, hi), d| (lo.min(d), hi.max(d)));
|
||||
if !(lo <= hi) {
|
||||
|
||||
@@ -21,7 +21,7 @@ fn check_partial_face_contact_pattern() {
|
||||
let (mut volume, mut area, mut shells) = (0.0, 0.0, 0);
|
||||
for b in &s.model.state().bodies {
|
||||
assert!(b.body.validity().is_empty(), "{:?}", b.body.validity());
|
||||
let m = b.body.tessellate(1e-5).unwrap().measure();
|
||||
let m = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure();
|
||||
volume += m.volume;
|
||||
area += m.area;
|
||||
shells += b.body.shell_count();
|
||||
@@ -55,7 +55,7 @@ fn intersection_never_keeps_material_outside_the_trimming_cylinder() {
|
||||
let before = volume(&s, "Base");
|
||||
let r = s.execute("solid.cylinder", &json!({"radius": 16.65, "height": 14.59, "operation": "intersect", "targets": ["Base"]}));
|
||||
let state = s.model.state();
|
||||
let reach = state.body("Base").unwrap().body.tessellate(1e-3).unwrap().positions.iter().map(|p| p.x.hypot(p.y)).fold(0.0, f64::max);
|
||||
let reach = state.body("Base").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap().positions.iter().map(|p| p.x.hypot(p.y)).fold(0.0, f64::max);
|
||||
match r {
|
||||
Ok(_) => assert!(reach < 16.65 + 1e-2, "kept material out to radius {reach}"),
|
||||
Err(e) => assert!((volume(&s, "Base") - before).abs() < 1e-9 && reach > 16.9, "{e}"),
|
||||
@@ -217,7 +217,7 @@ fn shallow_cuts_rebuild_after_inserting_an_earlier_pocket() {
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
let actual = volume(&session, "Plate");
|
||||
assert!((actual - expected).abs() < 0.02, "{actual} vs {expected}");
|
||||
let mesh = body.tessellate(0.001).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap();
|
||||
assert!(!mesh.contains(solidcraft_geom::Vec3::new(12.0, 14.0, 9.0)));
|
||||
assert!(mesh.contains(solidcraft_geom::Vec3::new(12.0, 14.0, 8.0)));
|
||||
assert!(!mesh.contains(solidcraft_geom::Vec3::new(20.0, 20.0, 9.5)));
|
||||
@@ -237,7 +237,7 @@ fn eighteen_tabs_join_the_ribs_of_a_chamfered_base() {
|
||||
let body = &state.body("Base").unwrap().body;
|
||||
assert_eq!(body.shell_count(), 1);
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
let mesh = body.tessellate(1e-4).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap();
|
||||
// R40 x 4 disc less its 1 x 1 chamfer, six 32 x 2 ribs
|
||||
// standing 4 above it, and eighteen 1 x 2 tabs another 8 higher.
|
||||
let expected = std::f64::consts::PI * (6400.0 - 40.0 + 1.0 / 3.0) + 1536.0 + 288.0;
|
||||
@@ -417,7 +417,7 @@ fn ribs_flush_with_a_window_change_nothing_away_from_them() {
|
||||
let area = 9.6 + 52.2 + 70.2 + 29.34 - 8.1 - 18.0 - 1.5;
|
||||
assert!((m.area - (a0 + n * area)).abs() < 1e-6, "rib {}: area {} (no slit adds any)", i + 1, m.area - a0);
|
||||
}
|
||||
let bounds = s.model.state().body("Lid").unwrap().body.tessellate(0.01).unwrap().bounds();
|
||||
let bounds = s.model.state().body("Lid").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap().bounds();
|
||||
assert!((bounds.max.z - 14.1).abs() < 1e-9, "{}", bounds.max.z);
|
||||
}
|
||||
|
||||
@@ -502,7 +502,7 @@ fn lip_swept_flush_round_a_corner_joins() {
|
||||
assert_eq!(b.body.shell_count(), 1);
|
||||
let block = (50.0 * 40.0 - (4.0 - std::f64::consts::PI) * 4.5 * 4.5) * 10.0;
|
||||
let lip = 41.0 + 31.0 + 41.0 + 2.0 * std::f64::consts::PI * (4.5 * 4.5 - 3.5 * 3.5) / 4.0;
|
||||
let volume = b.body.tessellate(1e-5).unwrap().measure().volume;
|
||||
let volume = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure().volume;
|
||||
assert!((volume - block - lip).abs() < 0.02, "{volume} expected {}", block + lip);
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ fn cut_of_a_whole_face_beside_round_corners_after_a_chamfer() {
|
||||
let block = (40.0 * 40.0 - (4.0 - pi) * 8.0 * 8.0) * 15.0;
|
||||
// The chamfer's 2 × 2 triangle swept along the loop (Pappus: its centroid is 2/3 mm in).
|
||||
let chamfer = 2.0 * (4.0 * 24.0 + 2.0 * pi * (8.0 - 2.0 / 3.0));
|
||||
let mesh = b.body.tessellate(1e-4).unwrap();
|
||||
let mesh = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap();
|
||||
let volume = mesh.measure().volume;
|
||||
assert!((volume - (block - chamfer)).abs() < 0.05, "{volume} expected {}", block - chamfer);
|
||||
let top = mesh.bounds().max.z;
|
||||
@@ -544,7 +544,7 @@ fn joined_loft_frames_plan_their_display_at_every_quality() {
|
||||
let body = &state.body(name).unwrap().body;
|
||||
for q in DisplayQuality::ALL {
|
||||
for k in 0..=MAX_REFINE {
|
||||
let plan = body.display_plan_angular(q.refined_chord(body.size(), k), q.refined_angle(k));
|
||||
let plan = body.mesh_plan(&solidcraft_kernel::MeshTolerance { chord: q.refined_chord(body.size(), k), angle: Some(q.refined_angle(k)) });
|
||||
assert!(plan.is_ok(), "{name} at {} + {k}: {:?}", q.id(), plan.err());
|
||||
}
|
||||
}
|
||||
@@ -569,7 +569,7 @@ fn oblique_cuts_through_a_chamfered_ring_are_exact() {
|
||||
assert!(reply["warning"].is_null(), "{reply}");
|
||||
}
|
||||
if matches!(step["command"].as_str(), Some("solid.move" | "solid.extrude")) {
|
||||
volumes.push(s.model.state().body("R").unwrap().body.tessellate(1e-5).unwrap().measure().volume);
|
||||
volumes.push(s.model.state().body("R").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure().volume);
|
||||
}
|
||||
}
|
||||
// Pappus: the profile's moment about the axis turned through 240°.
|
||||
@@ -595,7 +595,7 @@ fn oblique_cuts_through_a_chamfered_ring_are_exact() {
|
||||
// Faces meshed coarsely keep to their surfaces: chords cut a little area off round faces,
|
||||
// and nothing adds any (triangles fanning across a chamfer from the notch's hole or from the
|
||||
// cuts' trims, folded over each other, added a few mm² each).
|
||||
let (coarse, fine) = (body.tessellate(0.05).unwrap().measure().area, body.tessellate(1e-5).unwrap().measure().area);
|
||||
let (coarse, fine) = (body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.05)).unwrap().measure().area, body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure().area);
|
||||
assert!(coarse < fine && coarse > fine - 2.0, "area meshed at 0.05: {coarse} vs {fine}");
|
||||
println!(
|
||||
"ring {whole:.6} / {ring:.6} mm³; cuts {cut1:.6}, {cut2:.6}; notch {:.6} / {notch:.6}; area meshed at 0.05 {coarse:.4} / {fine:.4}",
|
||||
@@ -611,7 +611,7 @@ fn notch_into_a_ring_chamfer_is_cut_exactly() {
|
||||
let script: Value = serde_json::from_str(include_str!("../../../examples/regressions/oblique_cuts_through_chamfered_ring.json")).unwrap();
|
||||
let steps = script.get("commands").and_then(Value::as_array).unwrap();
|
||||
let mut s = Session::default();
|
||||
let measure = |s: &Session| s.model.state().body("R").unwrap().body.tessellate(1e-5).unwrap().measure().volume;
|
||||
let measure = |s: &Session| s.model.state().body("R").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure().volume;
|
||||
s.run_script(&Value::Array(steps[..5].to_vec())).unwrap();
|
||||
let before = measure(&s);
|
||||
// The notch's sketch and its cut.
|
||||
@@ -703,7 +703,7 @@ fn box_intersect_splits_the_end_off_a_raked_wedge_after_its_end_rounds() {
|
||||
let state = s.model.state();
|
||||
let body = &state.body(&target).unwrap().body;
|
||||
assert!(body.validity().is_empty(), "lean {lean}, x >= {x0}, {op}: {:?}", body.validity());
|
||||
(solidcraft_kernel::measure(body).unwrap().volume, body.tessellate(1e-3).unwrap().bounds())
|
||||
(solidcraft_kernel::measure(body).unwrap().volume, body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap().bounds())
|
||||
};
|
||||
let (slice, at) = piece("intersect");
|
||||
let (rest, _) = piece("cut");
|
||||
@@ -757,7 +757,7 @@ fn closed_sweep_redefines_to_a_half_turn_torus_cut() {
|
||||
assert_eq!(s.model.state().bodies.len(), 1);
|
||||
let body = &s.model.state().bodies[0].body;
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
let mesh = body.tessellate(0.02).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.02)).unwrap();
|
||||
let expected = (std::f64::consts::PI * 64.0 - 64.0) * std::f64::consts::TAU * 30.0;
|
||||
assert!((mesh.measure().volume - expected).abs() < expected * 0.015);
|
||||
for (point, material) in [(solidcraft_geom::Vec3::new(30.0, 0.0, 0.0), false), (solidcraft_geom::Vec3::new(30.0, 0.0, 7.0), true)] {
|
||||
@@ -942,7 +942,7 @@ fn imported_rotated_face_extrusions_preserve_other_operations_and_bodies() {
|
||||
let reference = expected.body(&body.name).unwrap();
|
||||
// Fixed fine chord tolerance: cap subdivision can change the default
|
||||
// adaptive mesh's cylinder faceting without changing its analytic surface.
|
||||
let (a, b) = (body.body.tessellate(1e-5).unwrap().measure().volume, reference.body.tessellate(1e-5).unwrap().measure().volume);
|
||||
let (a, b) = (body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure().volume, reference.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure().volume);
|
||||
assert!((a - b).abs() < b.max(1.) * 1e-6, "{operation}: {} {a} vs {b}", body.name);
|
||||
assert!(body.body.validity().is_empty(), "{operation}: {}", body.name);
|
||||
let (a, b) = (body.mesh(), reference.mesh());
|
||||
@@ -957,7 +957,7 @@ fn imported_rotated_face_extrusions_preserve_other_operations_and_bodies() {
|
||||
let unchanged = &actual.body("Other").unwrap().body;
|
||||
assert_eq!(unchanged.face_count(), other.face_count());
|
||||
assert_eq!(unchanged.color(), other.color());
|
||||
assert_eq!(unchanged.tessellate(0.01).unwrap().positions, other.tessellate(0.01).unwrap().positions, "unaffected imported body");
|
||||
assert_eq!(unchanged.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap().positions, other.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap().positions, "unaffected imported body");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1135,7 +1135,7 @@ fn cut_up_to_the_plate_top_face_opens_the_hole() {
|
||||
// The same design without the cut.
|
||||
script["commands"].as_array_mut().unwrap().pop();
|
||||
let before = replay_value(&script);
|
||||
let fine = |s: &Session| s.model.state().body("Body7").unwrap().body.tessellate(1e-5).unwrap().measure().volume;
|
||||
let fine = |s: &Session| s.model.state().body("Body7").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure().volume;
|
||||
let (removed, want) = (fine(&before) - fine(&s), std::f64::consts::PI * 3.04 * 3.04 * 1.2);
|
||||
assert!((removed - want).abs() < 0.01, "removed {removed} mm³, want {want}");
|
||||
}
|
||||
@@ -1176,7 +1176,7 @@ fn cut_beside_a_sealed_void_keeps_the_body() {
|
||||
assert_eq!(body.shell_count(), if operation == "cut" { 2 } else { 1 });
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
// The revolved side's mesh stays 0.3 mm³ under its cylinder at any finer tolerance.
|
||||
body.tessellate(1e-5).unwrap().measure().volume
|
||||
body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-5)).unwrap().measure().volume
|
||||
};
|
||||
// A box over the bottom edge: the segment beyond x = 15, 5 high.
|
||||
let edge_box = [("solid.box", json!({"length": 15, "width": 60, "height": 10, "corner": [15, -30, -5], "operation": "new", "body_name": "K"}))];
|
||||
@@ -1228,8 +1228,8 @@ fn joined_loft_frames_reference_mesh_covers_each_face_once() {
|
||||
a
|
||||
};
|
||||
let size = body.size();
|
||||
let mesh = per_face(&body.reference_display_mesh_angular(size * 3e-4, 12f64.to_radians()).unwrap());
|
||||
let fine = per_face(&body.reference_display_mesh_angular(size * 3e-5, 3f64.to_radians()).unwrap());
|
||||
let mesh = per_face(&body.mesh(&solidcraft_kernel::MeshTolerance { chord: size * 3e-4, angle: Some(12f64.to_radians()) }).unwrap());
|
||||
let fine = per_face(&body.mesh(&solidcraft_kernel::MeshTolerance { chord: size * 3e-5, angle: Some(3f64.to_radians()) }).unwrap());
|
||||
for (fi, (m, f)) in mesh.iter().zip(&fine).enumerate() {
|
||||
assert!((m - f).abs() <= 0.01 * f.max(1.0), "{name} face {fi}: mesh {m:.0} mm², finer mesh {f:.0} mm²");
|
||||
}
|
||||
|
||||
@@ -489,7 +489,7 @@ fn clipped(body: &solidcraft_kernel::Body, region: Aabb3) -> Option<solidcraft_k
|
||||
/// `body` cut down to where it may meet `piece` (their boxes' common part, grown as
|
||||
/// [`shared_region`] grows it), when that leaves part of it out. `None`: use the whole body.
|
||||
fn narrowed(body: &solidcraft_kernel::Body, piece: &solidcraft_kernel::Body) -> Option<solidcraft_kernel::Body> {
|
||||
let bounds = |b: &solidcraft_kernel::Body| b.tessellate_with((b.size() * 1e-3).max(1e-4), false).ok().map(|m| m.bounds());
|
||||
let bounds = |b: &solidcraft_kernel::Body| b.mesh(&solidcraft_kernel::MeshTolerance::chord((b.size() * 1e-3).max(1e-4))).ok().map(|m| m.bounds());
|
||||
let (x, y) = (bounds(body)?, bounds(piece)?);
|
||||
let (min, max) = (x.min.max(y.min), x.max.min(y.max));
|
||||
if x.is_empty() || y.is_empty() || !(min.x <= max.x && min.y <= max.y && min.z <= max.z) {
|
||||
|
||||
@@ -161,7 +161,7 @@ fn compare_bodies(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Meshed within a quarter of the tolerance (the kernel goes no finer than a millionth of a
|
||||
// body's size), so two meshes of one surface agree well within it.
|
||||
let chord = tolerance / 4.0;
|
||||
let mut done = solidcraft_kernel::map_on_threads(&[ba, bb], 2, |b| (super::inspect::measure_json(b), b.body.tessellate_with(chord, true)))
|
||||
let mut done = solidcraft_kernel::map_on_threads(&[ba, bb], 2, |b| (super::inspect::measure_json(b), b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(chord))))
|
||||
.into_iter()
|
||||
.zip([&na, &nb]);
|
||||
let mut next = || -> Result<(Value, solidcraft_geom::Mesh)> {
|
||||
|
||||
@@ -677,8 +677,8 @@ pub(super) fn paste(
|
||||
let mut material_matches = true;
|
||||
let tol_a = (a.size() * 1e-3).max(1e-3).max(a.size() * 1e-6);
|
||||
let tol_b = (b.size() * 1e-3).max(1e-3).max(b.size() * 1e-6);
|
||||
let mesh_a = a.display_mesh(tol_a)?;
|
||||
let mesh_b = b.display_mesh(tol_b)?;
|
||||
let mesh_a = a.mesh(&solidcraft_kernel::MeshTolerance::chord(tol_a))?;
|
||||
let mesh_b = b.mesh(&solidcraft_kernel::MeshTolerance::chord(tol_b))?;
|
||||
// Precharge all 27 candidates, six possible replacements, their triangle
|
||||
// distances and the three containment rays before any proof traversal.
|
||||
proof_work = proof_work.saturating_add(mesh_a.triangles.len().saturating_add(mesh_b.triangles.len()).saturating_mul(27 * 7 * 4));
|
||||
|
||||
@@ -488,17 +488,17 @@ fn transferred_frozen_face_paint_ignores_consumed_legacy_offset() {
|
||||
#[test]
|
||||
fn boundary_material_proof_requires_resolvable_stock_and_void() {
|
||||
use solidcraft_geom::Vec3;
|
||||
let a = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap().tessellate(1e-3).unwrap();
|
||||
let b = solidcraft_kernel::box_solid(Vec3::new(1e-5, 0., 0.), Vec3::new(10.00001, 10., 10.)).unwrap().tessellate(1e-3).unwrap();
|
||||
let a = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap().mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
|
||||
let b = solidcraft_kernel::box_solid(Vec3::new(1e-5, 0., 0.), Vec3::new(10.00001, 10., 10.)).unwrap().mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
|
||||
let p = Vec3::new(5e-6, 5., 5.);
|
||||
assert_ne!(a.contains(p), b.contains(p));
|
||||
assert!((super::mesh_distance(&a, p) - 5e-6).abs() < 1e-12);
|
||||
assert!((super::mesh_distance(&b, p) - 5e-6).abs() < 1e-12);
|
||||
assert!(super::material_probe_matches(&a, &b, p, 1e-3, 1e-3));
|
||||
let shifted = solidcraft_kernel::box_solid(Vec3::new(1., 0., 0.), Vec3::new(11., 10., 10.)).unwrap().tessellate(1e-3).unwrap();
|
||||
let shifted = solidcraft_kernel::box_solid(Vec3::new(1., 0., 0.), Vec3::new(11., 10., 10.)).unwrap().mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
|
||||
assert!(!super::material_probe_matches(&a, &shifted, p, 1e-3, 1e-3));
|
||||
let thin = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 1e-4)).unwrap().tessellate(1e-6).unwrap();
|
||||
let thin_shifted = solidcraft_kernel::box_solid(Vec3::new(0., 0., 1e-4), Vec3::new(10., 10., 2e-4)).unwrap().tessellate(1e-6).unwrap();
|
||||
let thin = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 1e-4)).unwrap().mesh(&solidcraft_kernel::MeshTolerance::chord(1e-6)).unwrap();
|
||||
let thin_shifted = solidcraft_kernel::box_solid(Vec3::new(0., 0., 1e-4), Vec3::new(10., 10., 2e-4)).unwrap().mesh(&solidcraft_kernel::MeshTolerance::chord(1e-6)).unwrap();
|
||||
// Every resolvable replacement is void: thin ambiguous material is refused.
|
||||
assert!(!super::material_probe_matches(&thin, &thin_shifted, Vec3::new(5., 5., 5e-5), 1e-3, 1e-3));
|
||||
assert!(!super::material_probe_matches(&a, &b, p, f64::INFINITY, 1e-3));
|
||||
@@ -514,12 +514,12 @@ fn frozen_bore_boundary_copy_preserves_stock_void_and_reopen() {
|
||||
s
|
||||
};
|
||||
let mut s = bore(10.);
|
||||
let source = s.model.state().body("Drilled").unwrap().body.tessellate(1e-3).unwrap();
|
||||
let source = s.model.state().body("Drilled").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
|
||||
// The exact R4 wall passes through x6; a chord may end either side of that
|
||||
// cardinal angle, so its distance is bounded by the requested sag tolerance.
|
||||
assert!(super::mesh_distance(&source, Vec3::new(6., 10., 5.)) <= 1e-3);
|
||||
let shifted = bore(11.).model.state().body("Drilled").unwrap().body.tessellate(1e-3).unwrap();
|
||||
let lost = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(30., 20., 10.)).unwrap().tessellate(1e-3).unwrap();
|
||||
let shifted = bore(11.).model.state().body("Drilled").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
|
||||
let lost = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(30., 20., 10.)).unwrap().mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
|
||||
assert!(!super::material_probe_matches(&source, &shifted, Vec3::new(6.5, 10., 5.), 1e-3, 1e-3));
|
||||
assert!(!super::material_probe_matches(&source, &lost, Vec3::new(10., 10., 5.), 1e-3, 1e-3));
|
||||
let created = run(&mut s, "component.from_bodies", json!({"bodies":["Drilled"]}));
|
||||
@@ -528,7 +528,7 @@ fn frozen_bore_boundary_copy_preserves_stock_void_and_reopen() {
|
||||
let check = |session: &Session| {
|
||||
let state = session.model.state();
|
||||
let body = state.bodies.iter().find(|body| session.doc.model_body_component(body) == copied).unwrap();
|
||||
let mesh = body.body.tessellate(1e-3).unwrap();
|
||||
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
|
||||
for (point, stock) in
|
||||
[(Vec3::new(5.5, 10., 5.), true), (Vec3::new(6.5, 10., 5.), false), (Vec3::new(10., 10., 5.), false), (Vec3::new(18., 10., 5.), true)]
|
||||
{
|
||||
@@ -555,7 +555,7 @@ fn frozen_r6_bore_copy_uses_current_geometry_despite_mesh_measure_bias() {
|
||||
let state = session.model.state();
|
||||
let body = state.bodies.iter().find(|b| session.doc.model_body_component(b) == copied).unwrap();
|
||||
assert_eq!(solidcraft_kernel::certified_prism_equivalent(&source, &body.body).unwrap(), Some(true));
|
||||
let mesh = body.body.tessellate(1e-3).unwrap();
|
||||
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
|
||||
for (point, material) in
|
||||
[(Vec3::new(3., 10., 5.), true), (Vec3::new(5., 10., 5.), false), (Vec3::new(10., 10., 5.), false), (Vec3::new(18., 10., 5.), true)]
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ fn painted_radius(session: &Session, name: &str, radius: f64) -> usize {
|
||||
let body =
|
||||
state.body(name).unwrap_or_else(|| panic!("missing {name}; bodies {:?}", state.bodies.iter().map(|body| &body.name).collect::<Vec<_>>()));
|
||||
let painted = session.doc.face_colors(body);
|
||||
let mesh = body.body.tessellate(0.001).unwrap();
|
||||
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap();
|
||||
let inverse = solidcraft_doc::mat_inverse(&body.source_transform).unwrap();
|
||||
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
if painted.iter().any(|(index, _)| *index == *face as usize) {
|
||||
@@ -90,7 +90,7 @@ fn legacy_v9_imported_emboss_anchor_and_ordinary_face_paint_replay_without_renam
|
||||
let step = solidcraft_kernel::step_export(&[&cylinder], "legacy anchored cylinder").unwrap();
|
||||
let source = imported(&mut s, step, "Sleeve");
|
||||
run(&mut s, "material.assign", json!({"bodies":["Sleeve"],"material":"Steel"}));
|
||||
let before_volume = s.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let before_volume = s.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let legacy_names = solidcraft_doc::naming::face_names(s.model.state().body("Sleeve").unwrap());
|
||||
assert!(legacy_names.iter().all(|name| name.starts_with(&format!("F{source}:face:"))));
|
||||
run(&mut s, "appearance.assign", json!({"faces":[{"body":"Sleeve","point":[6,0,20]}],"appearance":"ABS - Blue"}));
|
||||
@@ -104,7 +104,7 @@ fn legacy_v9_imported_emboss_anchor_and_ordinary_face_paint_replay_without_renam
|
||||
run(&mut s, "solid.emboss", json!({"sketch":"Badge","profiles":"all","depth":1,"targets":["Sleeve"],"name":"Raised Badge"}));
|
||||
let anchored = s.doc.find_feature("Raised Badge").unwrap().clone();
|
||||
assert!(matches!(anchored.kind, FeatureKind::Emboss { face: None, .. }));
|
||||
let volume = s.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let volume = s.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((volume - std::f64::consts::PI * 2000. - 16.).abs() < 0.1, "volume {volume}");
|
||||
assert!((volume - before_volume - 16.).abs() < 0.01, "addedvolume {} baseline {before_volume}", volume - before_volume);
|
||||
let paint = s.doc.appearances.faces.clone();
|
||||
@@ -138,7 +138,7 @@ fn legacy_v9_imported_emboss_anchor_and_ordinary_face_paint_replay_without_renam
|
||||
assert!(reopened.model.result(anchored.id).unwrap().error.is_none());
|
||||
let state = reopened.model.state();
|
||||
let body = state.body("Sleeve").unwrap();
|
||||
assert!((body.body.tessellate_with(1e-4, true).unwrap().measure().volume - volume).abs() < 1e-8);
|
||||
assert!((body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume - volume).abs() < 1e-8);
|
||||
assert!(!reopened.doc.face_colors(body).is_empty());
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
|
||||
assert_eq!(feature.face_names, vec![name]);
|
||||
assert!(feature.face_names.iter().all(|name| names.contains(name)));
|
||||
let feature_id = feature.id;
|
||||
let volume = s.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let volume = s.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((volume - std::f64::consts::PI * 380. - 21.).abs() < 0.2, "volume {volume}");
|
||||
let mut legacy: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
|
||||
legacy["format"] = json!("solidcraft/9");
|
||||
@@ -203,7 +203,7 @@ fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
|
||||
}
|
||||
let reopened = Session::new(solidcraft_doc::Document::from_json(&legacy.to_string()).unwrap());
|
||||
assert!(reopened.model.result(feature_id).unwrap().error.is_none());
|
||||
assert!((reopened.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume - volume).abs() < 1e-8);
|
||||
assert!((reopened.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume - volume).abs() < 1e-8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) struct ContactMeshes(HashMap<u64, ContactMesh>);
|
||||
/// Tessellate `body` for contact checks and measure how thin it is.
|
||||
fn contact_mesh(body: &solidcraft_doc::ModelBody) -> Result<ContactMesh> {
|
||||
let tolerance = (body.body.size() * 1e-3).max(1e-3);
|
||||
let mesh = body.body.tessellate(tolerance)?;
|
||||
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tolerance))?;
|
||||
if mesh.triangles.len() > MAX_TRIANGLES || mesh.positions.len().saturating_add(mesh.triangles.len().saturating_mul(4)) > 200_000 {
|
||||
return Err(limit("body exceeds contact sweep tessellation budget"));
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ fn redefining_a_fillet_as_g2_on_sloped_ends_survives_undo_and_reopen() {
|
||||
let volume = |s: &Session| {
|
||||
let bodies = &s.model.state().bodies;
|
||||
assert_eq!(bodies.len(), 1);
|
||||
bodies.first().unwrap().body.tessellate(0.0001).unwrap().measure().volume
|
||||
bodies.first().unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.0001)).unwrap().measure().volume
|
||||
};
|
||||
let expected = |r: f64| 12000.0 - 40.0 * 113.0 / 756.0 * r.powi(2) + 13711.0 / 486486.0 * r.powi(3);
|
||||
let actual = volume(&s);
|
||||
|
||||
@@ -529,7 +529,7 @@ fn cylinder_badge() -> Session {
|
||||
}
|
||||
|
||||
fn exact_cylinder_volume(s: &Session) -> f64 {
|
||||
s.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume
|
||||
s.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -546,7 +546,7 @@ fn cylinder_emboss_persists_selection_and_replays_radial_depth() {
|
||||
let state = s.model.state();
|
||||
let body = &state.body("Sleeve").unwrap().body;
|
||||
assert_eq!(body.shell_count(), 1);
|
||||
let mesh = body.tessellate_with(0.001, true).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap();
|
||||
assert!(mesh.contains(Vec3::new(-10.5 * 0.15_f64.sin(), 10.5 * 0.15_f64.cos(), 7.5)));
|
||||
let feature = s.doc.find_feature("Raised Badge").unwrap();
|
||||
assert!(
|
||||
@@ -630,7 +630,7 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
|
||||
.world_state()
|
||||
.bodies
|
||||
.iter()
|
||||
.find(|b| b.body.tessellate_with(0.001, true).unwrap().contains(solidcraft_geom::Vec3::new(40.5 + pick_offset, 0., 7.5)))
|
||||
.find(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().contains(solidcraft_geom::Vec3::new(40.5 + pick_offset, 0., 7.5)))
|
||||
.unwrap()
|
||||
.name
|
||||
.clone();
|
||||
@@ -654,7 +654,7 @@ fn cylinder_cross_component_emboss_keeps_source_owner_and_placements() {
|
||||
assert!((exact_cylinder_volume(&s) - before - 21.).abs() < 0.02);
|
||||
let world = s.world_state();
|
||||
for x in [39.5, 119.5] {
|
||||
assert!(world.bodies.iter().any(|b| b.body.tessellate_with(0.001, true).unwrap().contains(solidcraft_geom::Vec3::new(x, 0., 7.5))));
|
||||
assert!(world.bodies.iter().any(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().contains(solidcraft_geom::Vec3::new(x, 0., 7.5))));
|
||||
}
|
||||
let mut loaded = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Default::default() };
|
||||
loaded.refresh();
|
||||
@@ -802,7 +802,7 @@ fn check_body_offset(auto: bool) {
|
||||
assert!((exact_cylinder_volume(&s) - before - 21.).abs() < 0.02);
|
||||
assert_eq!(s.doc.body_offsets["Sleeve"], offset);
|
||||
assert_eq!(s.doc.body_component("Sleeve", s.model.state().body("Sleeve").unwrap().feature), holder);
|
||||
assert!(s.world_state().body("Sleeve").unwrap().body.tessellate_with(0.001, true).unwrap().contains(solidcraft_geom::Vec3::new(0., 10.5, 7.5)));
|
||||
assert!(s.world_state().body("Sleeve").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().contains(solidcraft_geom::Vec3::new(0., 10.5, 7.5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -984,7 +984,7 @@ fn cylinder_auto_inference_saves_a_profile_anchor_and_replays_both_depth_signs()
|
||||
let body = &state.body("Sleeve").unwrap().body;
|
||||
assert_eq!(body.shell_count(), 1);
|
||||
assert_eq!(
|
||||
body.tessellate_with(0.001, true).unwrap().contains(Vec3::new(0., if mode == "emboss" { 10.5 } else { 9.5 }, 7.5)),
|
||||
body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().contains(Vec3::new(0., if mode == "emboss" { 10.5 } else { 9.5 }, 7.5)),
|
||||
mode == "emboss"
|
||||
);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
@@ -1019,10 +1019,10 @@ fn cylinder_auto_inference_ignores_remote_finite_tangent_walls_in_each_body_orde
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[-2,5],"p1":[2,10]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let before = exact_cylinder_volume(&s);
|
||||
let remote = s.model.state().body("Remote").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let remote = s.model.state().body("Remote").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
run(&mut s, "solid.emboss", json!({"sketch":"Badge","depth":1}));
|
||||
assert!((exact_cylinder_volume(&s) - before - 21.).abs() < 0.02);
|
||||
assert_eq!(s.model.state().body("Remote").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume, remote);
|
||||
assert_eq!(s.model.state().body("Remote").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume, remote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1043,7 +1043,7 @@ fn cylinder_auto_inference_retains_selected_holes_and_nested_islands() {
|
||||
let st = s.model.state();
|
||||
let body = &st.body("Sleeve").unwrap().body;
|
||||
assert_eq!(body.shell_count(), 1);
|
||||
let mesh = body.tessellate_with(0.001, true).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap();
|
||||
for (s, raised) in [(0., true), (0.75, false), (1.5, true)] {
|
||||
let theta = s / 10.0_f64;
|
||||
let radius = if mode == "emboss" { 10.5 } else { 9.5 };
|
||||
@@ -1113,7 +1113,7 @@ fn loft_joined_onto_a_box_exports_one_connected_mesh() {
|
||||
let v = volume(&mut s);
|
||||
assert!(rel(v, 24000.0 + loft) < 1e-5, "{v} vs {}", 24000.0 + loft);
|
||||
// The exported mesh, its vertices merged, is one connected piece.
|
||||
let mesh = s.model.state().body("Box").unwrap().body.tessellate(0.01).unwrap();
|
||||
let mesh = s.model.state().body("Box").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let (positions, triangles) = solidcraft_io::weld(&mesh.positions, &mesh.triangles, 1e-4);
|
||||
let mut piece: Vec<usize> = (0..positions.len()).collect();
|
||||
let root = |piece: &mut Vec<usize>, mut i: usize| {
|
||||
@@ -1610,7 +1610,7 @@ fn knife_edge_part() -> Session {
|
||||
/// the concave arc). It holds to about 1e-3 of what a blend removes here; the bounds below are
|
||||
/// about twice what each test reads.
|
||||
fn planned(s: &Session) -> f64 {
|
||||
s.model.state().bodies.iter().map(|b| b.body.display_plan(2e-4).unwrap().evaluate().measure().volume).sum()
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh_plan(&solidcraft_kernel::MeshTolerance::chord(2e-4)).unwrap().evaluate().measure().volume).sum()
|
||||
}
|
||||
|
||||
/// The middle of the part's arc.
|
||||
|
||||
@@ -251,7 +251,7 @@ fn counterbore_floors_and_their_pattern_copies_take_a_sketch() {
|
||||
fn flat_faces(s: &Session) -> Vec<(usize, solidcraft_geom::Vec3)> {
|
||||
let state = s.model.state();
|
||||
let body = &state.bodies[0].body;
|
||||
let mesh = body.tessellate(0.01).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
(0..body.face_count())
|
||||
.filter(|index| body.planar_face(*index).is_some())
|
||||
.filter_map(|index| {
|
||||
|
||||
@@ -195,7 +195,7 @@ pub fn collisions(s: &Session) -> Vec<(String, String, f64)> {
|
||||
/// Edges are tested for crossing the other's surface too: plates crossing face to face have
|
||||
/// no sample point inside (#3323).
|
||||
fn overlap(a: &Body, b: &Body) -> Option<f64> {
|
||||
let (ma, mb) = (a.tessellate((a.size() * 1e-3).max(1e-3)).ok()?, b.tessellate((b.size() * 1e-3).max(1e-3)).ok()?);
|
||||
let (ma, mb) = (a.mesh(&solidcraft_kernel::MeshTolerance::chord((a.size() * 1e-3).max(1e-3))).ok()?, b.mesh(&solidcraft_kernel::MeshTolerance::chord((b.size() * 1e-3).max(1e-3))).ok()?);
|
||||
mesh_overlap(&ma, &mb, 1e-4 * a.size().max(b.size()).max(1.0))
|
||||
}
|
||||
|
||||
@@ -1001,7 +1001,7 @@ fn centres(s: &Session) -> Vec<(u64, Vec3)> {
|
||||
for o in s.doc.occurrences.iter().filter(|o| o.parent == 0) {
|
||||
let mut bounds = Aabb3::EMPTY;
|
||||
for (_, b) in bodies.iter().filter(|(path, _)| path.first() == Some(&o.id)) {
|
||||
if let Ok(m) = b.tessellate((b.size() * 1e-2).max(1e-2)) {
|
||||
if let Ok(m) = b.mesh(&solidcraft_kernel::MeshTolerance::chord((b.size() * 1e-2).max(1e-2))) {
|
||||
bounds = bounds.union(&m.bounds());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ fn check_centered(s: &Session, opposed: bool) {
|
||||
assert!(!body.contains(Vec3::new(0.0, 5.0, 3.0)));
|
||||
assert!(body.contains(Vec3::new(sign * 3.0 * (PI / 6.0).cos(), 5.0, 1.5)));
|
||||
assert!(body.contains(Vec3::new(sign * 3.0 * (PI / 6.0).cos(), 5.0, -1.5)));
|
||||
let bounds = body.tessellate(1e-4).unwrap().bounds();
|
||||
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().bounds();
|
||||
assert!((bounds.min.z + 2.0 * SQRT_2).abs() < 1e-6, "{bounds:?}");
|
||||
assert!((bounds.max.z - 2.0 * SQRT_2).abs() < 1e-6, "{bounds:?}");
|
||||
let inner_x = if sign > 0.0 { bounds.min.x } else { -bounds.max.x };
|
||||
@@ -80,7 +80,7 @@ fn two_sided_revolves_use_the_canonical_negative_and_positive_limits() {
|
||||
turn(&mut s, sign, json!({"direction":"positive","angle":"60 deg","angle2":"30 deg"}));
|
||||
let state = s.model.state();
|
||||
let body = &state.bodies[0].body;
|
||||
let bounds = body.tessellate(1e-4).unwrap().bounds();
|
||||
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().bounds();
|
||||
assert!((bounds.min.x - 1.0).abs() < 1e-6, "{bounds:?}");
|
||||
assert!((bounds.max.x - 4.0).abs() < 1e-4);
|
||||
assert!((bounds.min.z + 2.0).abs() < 1e-6);
|
||||
@@ -109,7 +109,7 @@ fn legacy_missing_convention_keeps_geometry_through_edit_redefine_undo_and_reope
|
||||
let body = &state.bodies[0].body;
|
||||
assert!(body.contains(Vec3::new(0.0, 5.0, 3.0)));
|
||||
assert!(!body.contains(Vec3::new(3.0, 5.0, 0.0)));
|
||||
let bounds = body.tessellate(1e-4).unwrap().bounds();
|
||||
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().bounds();
|
||||
assert!((bounds.min.x + 2.0 * SQRT_2).abs() < 1e-6);
|
||||
assert!((bounds.max.x - 2.0 * SQRT_2).abs() < 1e-6);
|
||||
assert!((bounds.min.z - SQRT_2).abs() < 1e-6);
|
||||
@@ -175,7 +175,7 @@ fn symmetric_revolve_preserves_a_translated_rotated_sketch_frame() {
|
||||
run(&mut s, "solid.revolve", json!({"sketch":"Profile","axis":{"origin":[10,20,30],"dir":[-1,0,0]},"angle":"90 deg","direction":"symmetric"}));
|
||||
let state = s.model.state();
|
||||
let body = &state.bodies[0].body;
|
||||
let bounds = body.tessellate(1e-4).unwrap().bounds();
|
||||
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().bounds();
|
||||
assert!(bounds.min.dist(Vec3::new(10.0, 20.0 - 2.0 * SQRT_2, 30.0 + SQRT_2)) < 1e-6, "{bounds:?}");
|
||||
assert!(bounds.max.dist(Vec3::new(20.0, 20.0 + 2.0 * SQRT_2, 34.0)) < 1e-4, "{bounds:?}");
|
||||
assert!(body.contains(Vec3::new(15.0, 20.0, 33.0)));
|
||||
|
||||
@@ -421,7 +421,7 @@ fn sheet_pocket_transfer_keeps_local_depth_world_pose_and_history() {
|
||||
assert!(mb.body.contains(floor));
|
||||
let world = s.world_state();
|
||||
let body = &world.body(name).unwrap().body;
|
||||
let bounds = body.tessellate(1e-3).unwrap().bounds();
|
||||
let bounds = body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap().bounds();
|
||||
assert!(bounds.min.dist(Vec3::ZERO) < 1e-6, "{bounds:?}");
|
||||
assert!(bounds.max.dist(Vec3::new(100.0, 60.0, 2.5)) < 1e-6, "{bounds:?}");
|
||||
assert!(!body.contains(Vec3::new(25.0, 25.0, 0.5)));
|
||||
@@ -4881,7 +4881,7 @@ fn fold_fixed_point_must_touch_the_sheet_but_any_face_will_do() {
|
||||
fn flat_face_points(s: &Session) -> Vec<(usize, solidcraft_geom::Vec3, solidcraft_geom::Vec3)> {
|
||||
let state = s.model.state();
|
||||
let body = &state.bodies[0].body;
|
||||
let mesh = body.tessellate(0.01).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
(0..body.face_count())
|
||||
.filter_map(|index| {
|
||||
let normal = body.planar_face(index)?.normal();
|
||||
|
||||
@@ -122,7 +122,7 @@ fn capture_impl(s: &Session, value: &Value, cmd: &str, internal: bool) -> Result
|
||||
continue;
|
||||
}
|
||||
let faces = body.body.faces(tolerance)?;
|
||||
let mesh = body.body.tessellate(tolerance)?;
|
||||
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tolerance))?;
|
||||
// The triangles are grouped by face once, and a face's test reads only its own: the
|
||||
// work is the mesh plus one plane test per face, not the mesh once per face (a
|
||||
// modelled thread leaves thousands of faces, #6720).
|
||||
|
||||
@@ -487,7 +487,7 @@ fn rounded_enclosure_shell_side_and_floor_attach_through_width_wall_and_reopen()
|
||||
assert!((state.sketch(side).unwrap().plane.origin.x - 50.).abs() < 1e-6);
|
||||
assert!((state.sketch(floor).unwrap().plane.origin.z - 3.).abs() < 1e-6);
|
||||
assert!(state.sketch(floor).unwrap().plane.normal().dot(Vec3::Z) > 0.999);
|
||||
let mesh = state.body("Case").unwrap().body.tessellate_with(1e-4, true).unwrap();
|
||||
let mesh = state.body("Case").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap();
|
||||
let case = &state.body("Case").unwrap().body;
|
||||
let corners: Vec<_> = (0..case.face_count()).filter(|i| matches!(solidcraft_kernel::face_surface(case, *i), Some(solidcraft_kernel::FaceSurface::Cylinder { radius, .. }) if (radius - 6.).abs() < 1e-6)).collect();
|
||||
assert_eq!(corners.len(), 4, "later port/boss features must not subdivide untouched outer corners");
|
||||
@@ -552,7 +552,7 @@ fn offset_rectangle_pocket_has_one_logical_outer_wall_and_closed_face_projection
|
||||
assert_eq!(solidcraft_kernel::step_export(&[&control], "control snapshot").unwrap(), before_control);
|
||||
let state = s.model.state();
|
||||
let body = state.body("Stock").unwrap();
|
||||
let display = body.body.display_mesh(0.01).unwrap();
|
||||
let display = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let face = body
|
||||
.body
|
||||
.faces(0.01)
|
||||
@@ -573,14 +573,14 @@ fn offset_rectangle_pocket_has_one_logical_outer_wall_and_closed_face_projection
|
||||
let sketch = &state.sketch(attached).unwrap().sketch;
|
||||
assert!(sketch.links.iter().all(|link| !link.lost));
|
||||
assert_eq!(sketch.curves.len(), 4, "whole outer-wall rectangle has no floor-height stations");
|
||||
let mesh = state.body("Stock").unwrap().body.display_mesh(0.01).unwrap();
|
||||
let mesh = state.body("Stock").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
assert!(mesh.contains(Vec3::new(0., 0., 0.5)));
|
||||
assert!(!mesh.contains(Vec3::new(0., 0., 5.)));
|
||||
assert!(mesh.contains(Vec3::new(28., 0., 5.)));
|
||||
let control = &state.body("Control").unwrap().body;
|
||||
assert_eq!(control.faces(0.01).unwrap().len(), 6);
|
||||
assert_eq!(control.edges(0.01).unwrap().len(), 12);
|
||||
let control_mesh = control.tessellate(0.01).unwrap();
|
||||
let control_mesh = control.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
assert!((control_mesh.measure().volume - 160.).abs() < 1e-6);
|
||||
assert!(control_mesh.contains(Vec3::new(102., 102., 0.5)));
|
||||
assert!(control_mesh.contains(Vec3::new(102., 102., 5.)));
|
||||
|
||||
@@ -662,7 +662,7 @@ fn a_rectangle_put_onto_a_projected_edge_stays_a_closed_profile() {
|
||||
// Picked by a point inside, it extrudes to a 20 x 15 x 10 block of its own.
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": id, "profiles": [{"point": [15, 7.5]}], "distance": 10, "operation": "new", "body_name": "Pad"}));
|
||||
let volume = |s: &Session, name: &str| s.model.state().body(name).unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let volume = |s: &Session, name: &str| s.model.state().body(name).unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((volume(&s, "Pad") - 3000.0).abs() < 1e-6, "{}", volume(&s, "Pad"));
|
||||
assert!((volume(&s, &block) - 24000.0).abs() < 1e-6, "{}", volume(&s, &block));
|
||||
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
|
||||
@@ -1876,7 +1876,7 @@ mod tests {
|
||||
fn patch_continuity_and_points_cap_a_tube() {
|
||||
// A coarse mesh is enough to tell the patches apart (measuring a fitted face's area
|
||||
// exactly tessellates it finely).
|
||||
let mesh = |s: &Session, name: &str| s.world_state().body(name).unwrap().body.tessellate(0.05).unwrap();
|
||||
let mesh = |s: &Session, name: &str| s.world_state().body(name).unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.05)).unwrap();
|
||||
let (mut s, rim) = open_tube();
|
||||
let reply = run(&mut s, "surface.patch", json!({"edges": rim, "body": "Tube", "continuity": "g1", "body_name": "Cap"}));
|
||||
assert_eq!(reply["continuity"], json!({"all": "g1"}), "{reply}");
|
||||
|
||||
@@ -115,7 +115,7 @@ fn a_network_of_3d_lines_is_a_hyperbolic_paraboloid() {
|
||||
// cent of the body); the measuring mesh is refined onto the surface.
|
||||
assert!((face - want).abs() < 1e-2 * want && (total - want).abs() < 1e-3 * want, "{face} / {total} vs {want}");
|
||||
let st = s.world_state();
|
||||
let mesh = st.body(&body).map(|b| b.body.tessellate(0.01)).and_then(Result::ok).unwrap_or_default();
|
||||
let mesh = st.body(&body).map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01))).and_then(Result::ok).unwrap_or_default();
|
||||
assert!(!mesh.positions.is_empty());
|
||||
for p in &mesh.positions {
|
||||
assert!((p.z - p.x * p.y / 10.0).abs() < 1e-6, "{p:?} is off the paraboloid");
|
||||
|
||||
@@ -92,7 +92,7 @@ fn twisted_sweep_faces_follow_the_profile_curves() {
|
||||
// The end cap turns with the twist: at 90 deg a square's corners are back where they were,
|
||||
// at 45 deg its top reaches out to the corner radius along the axes.
|
||||
let reach = |s: &Session| {
|
||||
let mesh = s.world_state().body("Twisted").unwrap().body.tessellate(0.01).unwrap();
|
||||
let mesh = s.world_state().body("Twisted").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
mesh.positions.iter().filter(|p| (p.z - 40.0).abs() < 1e-6).map(|p| p.x).fold(f64::MIN, f64::max)
|
||||
};
|
||||
assert!((reach(&s) - 5.0).abs() < 1e-3, "{}", reach(&s));
|
||||
|
||||
@@ -127,7 +127,7 @@ pub(super) fn prepare(s: &Session, tolerance: f64) -> Result<Scene> {
|
||||
let bytes = AtomicUsize::new(0);
|
||||
let results = map_with_local_pools(&items, WORKERS, |item| {
|
||||
progress.check().map_err(|e| KernelError::Failed(e.into()))?;
|
||||
let mut mesh = item.body.body.reference_display_mesh_angular(tolerance, solidcraft_kernel::DISPLAY_ANGLE)?;
|
||||
let mut mesh = item.body.body.mesh(&solidcraft_kernel::MeshTolerance { chord: tolerance, angle: Some(solidcraft_kernel::DISPLAY_ANGLE) })?;
|
||||
if mesh.triangles.is_empty() {
|
||||
return Err(KernelError::Invalid(format!("{} has no exportable surface", item.body.name)));
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ fn what_reads_the_display_mesh_of_a_large_sheet_answers_as_before() {
|
||||
let st = s.world_state();
|
||||
let b = &st.bodies[0];
|
||||
let m = b.mesh();
|
||||
let coarse = b.body.display_mesh((b.body.size() * 1e-3).max(1e-3)).unwrap();
|
||||
let coarse = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord((b.body.size() * 1e-3).max(1e-3))).unwrap();
|
||||
// Bounds: the sheet's box is set by flat faces, so the rounder bends move nothing.
|
||||
let (now, before) = (m.bounds(), coarse.bounds());
|
||||
assert!(now.min.dist(before.min) < 1e-9 && now.max.dist(before.max) < 1e-9, "{now:?} {before:?}");
|
||||
|
||||
@@ -144,7 +144,7 @@ fn enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen
|
||||
assert!((regions[0].area() - 332.570619298295).abs() < 1e-7, "area {}", regions[0].area());
|
||||
let ring = solidcraft_kernel::extrude(&solved.plane, ®ions, 0., 2.).unwrap();
|
||||
assert_eq!(ring.len(), 1);
|
||||
let precise_ring = ring[0].tessellate(1e-6).unwrap();
|
||||
let precise_ring = ring[0].mesh(&solidcraft_kernel::MeshTolerance::chord(1e-6)).unwrap();
|
||||
let bounds = precise_ring.bounds();
|
||||
for (actual, expected) in
|
||||
[(bounds.min.x, -37.8), (bounds.max.x, 37.8), (bounds.min.y, -32.8), (bounds.max.y, 32.8), (bounds.min.z, 28.), (bounds.max.z, 30.)]
|
||||
@@ -178,7 +178,7 @@ fn enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen
|
||||
(measured - analytic_volume).abs() <= discretization_bound,
|
||||
"ring mesh volume {measured}, analytic {analytic_volume}, bound {discretization_bound}"
|
||||
);
|
||||
let lid = state.body("Lid").unwrap().body.tessellate(1e-4).unwrap();
|
||||
let lid = state.body("Lid").unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap();
|
||||
for (point, material) in [
|
||||
(Vec3::new(0., 32.2, 29.), true),
|
||||
(Vec3::new(0., 32.9, 29.), false),
|
||||
|
||||
@@ -14,7 +14,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
}
|
||||
|
||||
fn volumes(s: &Session) -> Vec<f64> {
|
||||
s.model.state().bodies.iter().map(|b| b.body.tessellate(1e-2).unwrap().measure().volume).collect()
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-2)).unwrap().measure().volume).collect()
|
||||
}
|
||||
|
||||
fn last_error(s: &Session) -> Option<String> {
|
||||
|
||||
@@ -18,7 +18,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
}
|
||||
|
||||
fn volume(s: &Session) -> f64 {
|
||||
s.model.state().bodies.iter().map(|b| b.body.tessellate(1e-3).unwrap().measure().volume).sum()
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap().measure().volume).sum()
|
||||
}
|
||||
|
||||
fn result(s: &Session, name: &str) -> (Option<String>, Option<String>) {
|
||||
|
||||
@@ -41,7 +41,7 @@ fn enclosure() -> Session {
|
||||
}
|
||||
|
||||
fn bodies(s: &Session) -> Vec<(String, f64)> {
|
||||
s.model.state().bodies.iter().map(|b| (b.name.clone(), b.body.tessellate(1e-2).unwrap().measure().volume)).collect()
|
||||
s.model.state().bodies.iter().map(|b| (b.name.clone(), b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-2)).unwrap().measure().volume)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1456,7 +1456,7 @@ fn twisted_ruled_faces_are_meshed_to_the_chord_tolerance() {
|
||||
let st = s.model.state();
|
||||
let body = &st.bodies.first().unwrap().body;
|
||||
let exact = solidcraft_kernel::measure(body).unwrap().volume;
|
||||
let mesh = body.tessellate(0.05).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.05)).unwrap();
|
||||
let volume: f64 = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
|
||||
@@ -79,6 +79,6 @@ fn a_barrel_joined_between_the_sides_of_a_leaf_needs_no_retry() {
|
||||
}
|
||||
// A quarter of the barrel lies in the leaf.
|
||||
let expected = 5500. + 0.75 * PI * 25. * 14.;
|
||||
let actual = plate.tessellate(1e-4).unwrap().measure().volume;
|
||||
let actual = plate.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!(rel(actual, expected) < 1e-5, "{actual} vs {expected}");
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ fn core(p: Vec3) -> f64 {
|
||||
/// vertices were looked at and the worst error of that distance.
|
||||
fn off_the_ball(s: &Session, round: f64) -> (usize, f64) {
|
||||
let body = &s.model.state().bodies[0].body;
|
||||
let mesh = body.tessellate(0.01).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let centres: Vec<Vec3> = (0..720)
|
||||
.map(|i| {
|
||||
let a = 2.0 * PI * i as f64 / 720.0;
|
||||
|
||||
@@ -93,7 +93,7 @@ fn a_flange_flush_with_the_end_of_a_bent_tapered_tube_joins() {
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
assert_eq!(body.lumps().unwrap().len(), 1);
|
||||
let (origin, normal) = (Vec3::new(BEND * TURN.sin(), BEND * (1.0 - TURN.cos()), 0.0), Vec3::new(TURN.cos(), TURN.sin(), 0.0));
|
||||
let mesh = body.tessellate(0.05).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.05)).unwrap();
|
||||
let beyond = mesh.positions.iter().map(|p| (*p - origin).dot(normal)).fold(f64::MIN, f64::max);
|
||||
assert!(beyond < 1e-3, "the join reaches {beyond} mm past the end plane");
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ fn a_ring_section_keeps_its_hole_and_rejects_a_disc() {
|
||||
for face in 0..body.body.face_count() {
|
||||
assert!(solidcraft_doc::naming::face_name_exact(body, face), "face {face} lost its sketch-curve provenance");
|
||||
}
|
||||
assert!(!body.body.tessellate(0.001).unwrap().contains(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0)));
|
||||
assert!(!body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().contains(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0)));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.model.state().bodies.is_empty());
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
|
||||
@@ -4,7 +4,7 @@ use solidcraft_geom::Vec3;
|
||||
|
||||
fn imported_box() -> Session {
|
||||
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 20., 30.)).unwrap();
|
||||
let mesh = body.tessellate(0.01).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let mut doc = Document::default();
|
||||
doc.add_feature(
|
||||
FeatureKind::MeshImport {
|
||||
|
||||
@@ -460,7 +460,7 @@ fn knife_edge_part() -> Session {
|
||||
|
||||
/// The volume of the session's bodies through the display plan.
|
||||
fn planned_volume(s: &Session) -> f64 {
|
||||
s.model.state().bodies.iter().map(|b| b.body.display_plan(2e-4).unwrap().evaluate().measure().volume).sum()
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh_plan(&solidcraft_kernel::MeshTolerance::chord(2e-4)).unwrap().evaluate().measure().volume).sum()
|
||||
}
|
||||
|
||||
/// #851: the knife edge where the wall runs out has opposite normals on its two faces. It rounds
|
||||
@@ -538,7 +538,7 @@ fn blend_corners_where_edges_meet_on_a_curved_wall() {
|
||||
}
|
||||
for b in &s.model.state().bodies {
|
||||
assert!(b.body.validity().is_empty(), "{name}: {:?}", b.body.validity());
|
||||
assert!(b.body.display_plan(2e-4).is_ok(), "{name}: the display plan refuses the body");
|
||||
assert!(b.body.mesh_plan(&solidcraft_kernel::MeshTolerance::chord(2e-4)).is_ok(), "{name}: the display plan refuses the body");
|
||||
}
|
||||
let removed = base - planned_volume(&s);
|
||||
assert!(removed > 0.0 && removed < 250.0, "{name}: removed {removed}");
|
||||
@@ -570,7 +570,7 @@ fn seed_33_display_plan_and_measure_agree() {
|
||||
assert_eq!(r.outcome.kind, "ok", "seed 33: {:?}\n{}", r.outcome, r.script());
|
||||
let s = script(r.script());
|
||||
for b in &s.model.state().bodies {
|
||||
let planned = b.body.display_plan(2e-4).unwrap().evaluate().measure().volume;
|
||||
let planned = b.body.mesh_plan(&solidcraft_kernel::MeshTolerance::chord(2e-4)).unwrap().evaluate().measure().volume;
|
||||
let measured = solidcraft_kernel::measure(&b.body).unwrap().volume;
|
||||
assert!((planned - measured).abs() < 2e-5 * measured, "{}: planned {planned}, measured {measured}", b.name);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
/// measurement of its thousands of faces.
|
||||
fn volume(s: &mut Session, b: &str) -> f64 {
|
||||
let st = s.model.state();
|
||||
st.body(b).map(|mb| mb.body.tessellate(1e-3).unwrap().measure().volume).unwrap_or(0.0)
|
||||
st.body(b).map(|mb| mb.body.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap().measure().volume).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
const SIZES: [(f64, f64); 9] = [(3.0, 0.5), (4.0, 0.7), (5.0, 0.8), (6.0, 1.0), (8.0, 1.25), (10.0, 1.5), (12.0, 1.75), (16.0, 2.0), (20.0, 2.5)];
|
||||
@@ -319,8 +319,8 @@ fn faceted_flange_thread_preserves_thin_crest_boundaries() {
|
||||
let groove = kernel::sweep_helix_mesh(&plane, §ion, base, *cylinder.axis, pitch, (end - start) / pitch, 48).unwrap();
|
||||
let cut = kernel::faceted_boolean(body, &groove, kernel::BoolOp::Cut, pitch * 0.005).unwrap().unwrap();
|
||||
assert!(cut.validity().is_empty(), "{:?}", cut.validity());
|
||||
let before = body.tessellate(0.001).unwrap().measure().volume;
|
||||
let after = cut.tessellate(0.001).unwrap().measure().volume;
|
||||
let before = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume;
|
||||
let after = cut.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume;
|
||||
assert!(before - after > 700.0 && before - after < 1800.0, "removed {}", before - after);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ fn a_pipeline_the_device_rejects_is_an_error_not_a_crash() {
|
||||
// The same device then builds the real pipeline and evaluates on it.
|
||||
let gpu = ok(TessGpu::new(name, device, queue));
|
||||
let body = ok(solidcraft_kernel::torus(Vec3::ZERO, 6.0, 1.5));
|
||||
let plan = ok(body.display_plan(0.01));
|
||||
let plan = ok(body.mesh_plan(&solidcraft_kernel::MeshTolerance::chord(0.01)));
|
||||
assert_eq!(ok(gpu.evaluate(&plan)).positions.len(), plan.evaluate().positions.len());
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ fn bodies() -> Vec<(&'static str, Body)> {
|
||||
fn gpu_evaluation_matches_the_cpu() {
|
||||
let Some(gpu) = gpu() else { return };
|
||||
for (name, body) in bodies() {
|
||||
let plan = ok(body.display_plan(0.01));
|
||||
let plan = ok(body.mesh_plan(&solidcraft_kernel::MeshTolerance::chord(0.01)));
|
||||
let cpu = plan.evaluate();
|
||||
let got = ok(gpu.evaluate(&plan));
|
||||
assert_eq!(got.triangles, cpu.triangles, "{name}");
|
||||
@@ -155,7 +155,7 @@ fn gpu_evaluation_matches_the_cpu() {
|
||||
fn a_resident_mesh_matches_the_cpu() {
|
||||
let Some(gpu) = gpu() else { return };
|
||||
for (name, body) in bodies() {
|
||||
let plan = ok(body.display_plan_angular(0.01, std::f64::consts::PI / 9.0));
|
||||
let plan = ok(body.mesh_plan(&solidcraft_kernel::MeshTolerance { chord: 0.01, angle: Some(std::f64::consts::PI / 9.0) }));
|
||||
let cpu = plan.evaluate();
|
||||
let mesh = ok(gpu.prepare(&plan));
|
||||
assert_eq!(mesh.vertex_count as usize, cpu.positions.len(), "{name}");
|
||||
@@ -196,7 +196,7 @@ fn a_resident_mesh_matches_the_cpu() {
|
||||
#[test]
|
||||
fn a_scene_in_one_dispatch_matches_each_body_on_the_cpu() {
|
||||
let Some(gpu) = gpu() else { return };
|
||||
let plans: Vec<TessPlan> = bodies().iter().map(|(_, b)| ok(b.display_plan_angular(0.01, std::f64::consts::PI / 9.0))).collect();
|
||||
let plans: Vec<TessPlan> = bodies().iter().map(|(_, b)| ok(b.mesh_plan(&solidcraft_kernel::MeshTolerance { chord: 0.01, angle: Some(std::f64::consts::PI / 9.0) }))).collect();
|
||||
let refs: Vec<&TessPlan> = plans.iter().collect();
|
||||
let mesh = ok(gpu.prepare_many(&refs));
|
||||
assert_eq!(mesh.parts.len(), plans.len());
|
||||
@@ -300,11 +300,11 @@ fn bench_plate() {
|
||||
let (mut nr, mut np) = (0, 0);
|
||||
for _ in 0..5 {
|
||||
let t = std::time::Instant::now();
|
||||
let m = ok(plate.tessellate(tol));
|
||||
let m = ok(plate.mesh(&solidcraft_kernel::MeshTolerance::chord(tol)));
|
||||
r.push(ms(t));
|
||||
nr = m.triangles.len();
|
||||
let t = std::time::Instant::now();
|
||||
let plan = ok(plate.display_plan(tol));
|
||||
let plan = ok(plate.mesh_plan(&solidcraft_kernel::MeshTolerance::chord(tol)));
|
||||
p.push(ms(t));
|
||||
let t = std::time::Instant::now();
|
||||
let m = plan.evaluate();
|
||||
|
||||
@@ -391,7 +391,7 @@ pub fn export_mesh(b: &solidcraft_kernel::Body) -> Result<Mesh> {
|
||||
|
||||
/// [`export_mesh`] at `quality` (never finer than 1 µm).
|
||||
pub fn export_mesh_at(b: &solidcraft_kernel::Body, quality: MeshQuality) -> Result<Mesh> {
|
||||
Ok(b.tessellate((b.size() * quality.share()).max(1e-3))?)
|
||||
Ok(b.mesh(&solidcraft_kernel::MeshTolerance::chord((b.size() * quality.share()).max(1e-3)))?)
|
||||
}
|
||||
|
||||
/// Distance within which an exported mesh's vertices are welded (STL and 3MF alike).
|
||||
|
||||
@@ -23,7 +23,6 @@ thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
truck-modeling = { workspace = true }
|
||||
truck-shapeops = { workspace = true }
|
||||
truck-meshalgo = { workspace = true }
|
||||
truck-stepio = { workspace = true }
|
||||
truck-topology = { workspace = true }
|
||||
spade = { workspace = true }
|
||||
@@ -33,7 +32,4 @@ web-time = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# The vendored crate tests never build in CI, so its equivalence checks run from kernel tests.
|
||||
# Resolver 3 keeps this dev-only feature out of shipped builds.
|
||||
truck-meshalgo = { workspace = true, features = ["test-hooks"] }
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
|
||||
+13
-13
@@ -2520,7 +2520,7 @@ fn chamfer_tool_curved(cur: &Body, pick: Vec3, distance: f64) -> Result<Cutter>
|
||||
// Contacts must lie on the trimmed faces, not merely their infinite supports. A
|
||||
// fine mesh and an inward probe larger than its chord error also reject a bevel that
|
||||
// has already used up the top face. Reuse that mesh for all cutter clearance probes.
|
||||
let mesh = cur.tessellate_with(tolerance * 10.0, true)?;
|
||||
let mesh = cur.mesh(&crate::MeshTolerance::chord(tolerance * 10.0))?;
|
||||
let inset = (tolerance * 100.0).min(distance * 0.01);
|
||||
for (face, point) in [(*f1, a), (*f2, b)] {
|
||||
let (_, normal, _) = crate::curveblend::project(face, point, None).ok_or_else(|| unsupported("chamfer contact"))?;
|
||||
@@ -2681,7 +2681,7 @@ mod tests {
|
||||
assert_eq!(sections, 2);
|
||||
// Independent analytic area: integrating x(t) y'(t) for the quintic
|
||||
// gives 113/756 times setback² for a right-angle corner.
|
||||
let volume = out.tessellate(0.0001).unwrap().measure().volume;
|
||||
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
let expected = 8000.0 - (113.0 / 756.0) * 4.0 * 20.0;
|
||||
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
|
||||
}
|
||||
@@ -2699,7 +2699,7 @@ mod tests {
|
||||
let body = crate::build::extrude(&Plane::XY, &[Region2 { outer: lp, holes: vec![] }], 0.0, 20.0).unwrap().remove(0);
|
||||
let out = fillet_g2(&body, &[Vec3::new(10.0, 10.0, 10.0)], 2.0).unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
let volume = out.tessellate(0.0001).unwrap().measure().volume;
|
||||
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
let expected = 6000.0 + (113.0 / 756.0) * 4.0 * 20.0;
|
||||
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
|
||||
let body = crate::build::box_solid(Vec3::ZERO, Vec3::new(20.0, 20.0, 20.0)).unwrap();
|
||||
@@ -2799,7 +2799,7 @@ mod tests {
|
||||
// the quintic's x(t)y'(t) and x(t)y(t)y'(t). Axial length is 40−(left+right)y.
|
||||
let removed = 40.0 * 113.0 / 756.0 * 25.0 - (left + right) * 13711.0 / 486486.0 * 125.0;
|
||||
let expected = 16000.0 - 4000.0 * (left + right) - removed;
|
||||
let volume = out.tessellate(0.0001).unwrap().measure().volume;
|
||||
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
assert!((volume - expected).abs() < 0.05, "{volume} vs {expected}");
|
||||
}
|
||||
}
|
||||
@@ -2840,7 +2840,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
let volume = out.tessellate(0.0001).unwrap().measure().volume;
|
||||
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
let expected = 6000.0 + 113.0 / 756.0 * 4.0 * 20.0;
|
||||
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
|
||||
}
|
||||
@@ -2894,7 +2894,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_blend_runs_into_a_cylinder_at_its_end() {
|
||||
let body = box_and_boss();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let vol = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
// The edge (40, 0..8, 0) ends in the cylinder wall at y = 8.
|
||||
let len = 20.0 - (144.0f64 - 0.0).sqrt();
|
||||
@@ -2915,7 +2915,7 @@ mod tests {
|
||||
let plate = crate::build::box_solid(Vec3::ZERO, Vec3::new(40.0, 20.0, 10.0)).unwrap();
|
||||
let hole = crate::build::cylinder(Vec3::new(20.0, 0.0, -1.0), Vec3::Z, 5.0, 12.0).unwrap();
|
||||
let body = crate::ops::boolean(&plate, &hole, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let vol = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
for (shape, area) in [(Shape::Round, (1.0 - std::f64::consts::PI / 4.0)), (Shape::Flat, 0.5)] {
|
||||
let out = blend(&body, &[Vec3::new(5.0, 0.0, 10.0)], 1.0, shape, "blend").unwrap();
|
||||
@@ -2930,7 +2930,7 @@ mod tests {
|
||||
let plate = crate::build::box_solid(Vec3::ZERO, Vec3::new(40.0, 40.0, 10.0)).unwrap();
|
||||
let pocket = crate::build::box_solid(Vec3::new(10.0, 10.0, -1.0), Vec3::new(30.0, 30.0, 12.0)).unwrap();
|
||||
let body = crate::ops::boolean(&plate, &pocket, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let vol = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
let one = chamfer(&body, &[Vec3::new(10.0, 10.0, 5.0)], 2.0).unwrap();
|
||||
assert!(one.validity().is_empty(), "{:?}", one.validity());
|
||||
@@ -2959,7 +2959,7 @@ mod tests {
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
assert_eq!(out.lumps().unwrap().len(), 1);
|
||||
assert_eq!(out.face_count(), c.face_count() + 1);
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let vol = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
// About the triangle (sides 1 and 1 at 120°) along the mitre; its ends slant.
|
||||
let removed = vol(&c) - vol(&out);
|
||||
let along = 0.5 * 120f64.to_radians().sin() * k * 3f64.sqrt();
|
||||
@@ -2973,7 +2973,7 @@ mod tests {
|
||||
let b = crate::build::box_solid(Vec3::ZERO, Vec3::new(40.0, 20.0, 20.0)).unwrap();
|
||||
let c = crate::build::cylinder(Vec3::new(20.0, -1.0, 16.0), Vec3::Y, 8.0, 22.0).unwrap();
|
||||
let body = crate::ops::boolean(&b, &c, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let vol = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
for shape in [Shape::Round, Shape::Flat] {
|
||||
let out = blend(&body, &[Vec3::new(20.0, 0.0, 8.0)], 0.5, shape, "blend").unwrap();
|
||||
@@ -3007,7 +3007,7 @@ mod tests {
|
||||
// wall is in pieces (inside the box it is gone): the blend's end lies on the cylinder
|
||||
// where the box was, now laid bare.
|
||||
let body = box_and_boss();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let vol = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
for (shape, area) in [(Shape::Round, (1.0 - std::f64::consts::PI / 4.0) * 0.64), (Shape::Flat, 0.32)] {
|
||||
let out = blend(&body, &[Vec3::new(40.0, 4.0, 20.0)], 0.8, shape, "blend").unwrap();
|
||||
@@ -3024,7 +3024,7 @@ mod tests {
|
||||
// The boss's bottom edge outside the box (a half circle) ends at the box's side, square to
|
||||
// it: the side face takes the end section, its bottom edge running on to the blend.
|
||||
let body = box_and_boss();
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let vol = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
for (shape, area, c) in [
|
||||
(
|
||||
@@ -3071,7 +3071,7 @@ mod tests {
|
||||
})
|
||||
});
|
||||
assert!(seam_twice);
|
||||
let vol = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume;
|
||||
let vol = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let v0 = vol(&body);
|
||||
// The edge runs from x = −45 to where the wall meets the cylinder, x = 55 − √(25² − 5²).
|
||||
let len = 45.0 + 55.0 - (600f64).sqrt();
|
||||
|
||||
@@ -1497,7 +1497,7 @@ mod tests {
|
||||
assert!(result.validity().is_empty());
|
||||
assert_eq!(result.unmeshed_faces(), 0);
|
||||
assert!(vol(&result) < vol(&basin));
|
||||
let mesh = result.tessellate(0.01).unwrap();
|
||||
let mesh = result.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
assert!(mesh.contains(Vec3::new(50.0, 2.0, 27.0)));
|
||||
assert!(!mesh.contains(Vec3::new(50.0, 3.9, 29.99)));
|
||||
// The small blend removes only a strip at the rim, and never escapes
|
||||
|
||||
@@ -271,7 +271,7 @@ fn select(body: &Body, pts: &[Vec3]) -> Result<Selection> {
|
||||
fn select_in(body: &Body, pts: &[Vec3]) -> Result<Selection> {
|
||||
let size = body.size();
|
||||
let chord = (size * 1e-3).max(1e-4);
|
||||
let mesh = body.tessellate(chord)?;
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(chord))?;
|
||||
let solid = &*body.solid;
|
||||
let faces: Vec<&mt::Face> = solid.face_iter().collect();
|
||||
let planar: Vec<bool> = faces.iter().map(|f| plane_normal(f).is_some()).collect();
|
||||
@@ -617,7 +617,7 @@ fn volume_in(sel: &Selection, input: &Body, out: Body, size: Size) -> Result<Bod
|
||||
}
|
||||
// Well within the bound on the meshes the check made anyway: accepted. Otherwise measured
|
||||
// finely, as a coarse mesh of a cone or a blend is off by more than a small blend.
|
||||
if let Ok(m) = out.tessellate(sel.chord).map(|m| m.measure()) {
|
||||
if let Ok(m) = out.mesh(&crate::MeshTolerance::chord(sel.chord)).map(|m| m.measure()) {
|
||||
let (va, aa) = sel.coarse;
|
||||
// A chord's sag over every face bounds what the meshes miss.
|
||||
let slack = (aa + m.area) * sel.chord;
|
||||
|
||||
@@ -108,7 +108,7 @@ mod imp {
|
||||
if picks.iter().any(|p| !p.is_finite()) {
|
||||
return Err(KernelError::Invalid("edge picks must be finite points".into()));
|
||||
}
|
||||
let mesh = body.tessellate((body.size() * 2e-3).max(1e-3))?;
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord((body.size() * 2e-3).max(1e-3)))?;
|
||||
let seams = crate::topo::seam_flags(body, &mesh);
|
||||
let (smooth, _) = crate::smooth_edges::flags(body, &mesh);
|
||||
let reach = pick_tolerance(body);
|
||||
|
||||
@@ -806,7 +806,7 @@ mod tests {
|
||||
/// The volume of a fine tessellation: at a blend closed to a point `measure` takes a
|
||||
/// coarser mesh of the blend, a few mm³ off on these parts.
|
||||
fn fine_volume(b: &Body) -> f64 {
|
||||
b.tessellate_with(5e-4, true).unwrap().measure().volume
|
||||
b.mesh(&crate::MeshTolerance::chord(5e-4)).unwrap().measure().volume
|
||||
}
|
||||
|
||||
/// A taper on the top front edge of a wedge, whose top face narrows to a point, and of a
|
||||
|
||||
+131
-424
@@ -88,7 +88,7 @@ pub(crate) struct Tolerances {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MeshMemo {
|
||||
solid: std::sync::Arc<Solid>,
|
||||
made: std::sync::Mutex<Vec<((u64, u64, bool), std::sync::Arc<Mesh>)>>,
|
||||
made: Remembered,
|
||||
/// The meshes a boolean makes of the solid's shells, by tolerance.
|
||||
shells: std::sync::Mutex<Vec<(u64, std::sync::Arc<Vec<truck_shapeops::MeshedShell>>)>>,
|
||||
}
|
||||
@@ -96,17 +96,37 @@ pub(crate) struct MeshMemo {
|
||||
/// Meshes a [`MeshMemo`] keeps at most (the oldest goes first).
|
||||
const MEMO_MESHES: usize = 4;
|
||||
|
||||
/// Meshes kept for reuse, by tolerance (see [`remembered`]).
|
||||
type Remembered = std::sync::Mutex<Vec<((u64, u64), std::sync::Arc<Mesh>)>>;
|
||||
|
||||
/// The mesh at `t` from `kept`, made by `make` and kept (at most [`MEMO_MESHES`]) if it isn't
|
||||
/// there. The lock is held while meshing: threads asking for the same mesh wait for it rather
|
||||
/// than make it too.
|
||||
fn remembered(kept: &Remembered, t: &MeshTolerance, make: impl FnOnce() -> Result<Mesh>) -> Result<std::sync::Arc<Mesh>> {
|
||||
let Ok(mut made) = kept.lock() else { return make().map(std::sync::Arc::new) };
|
||||
let key = t.key();
|
||||
if let Some((_, m)) = made.iter().find(|(k, _)| *k == key) {
|
||||
return Ok(m.clone());
|
||||
}
|
||||
let m = std::sync::Arc::new(make()?);
|
||||
if made.len() >= MEMO_MESHES {
|
||||
made.remove(0);
|
||||
}
|
||||
made.push((key, m.clone()));
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// The display mesh of a body that several placed copies show (see `Body::display_of`): the
|
||||
/// body, and its mesh once made.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SharedDisplay {
|
||||
pub(crate) original: Body,
|
||||
mesh: std::sync::OnceLock<Option<Mesh>>,
|
||||
meshes: Remembered,
|
||||
}
|
||||
|
||||
impl SharedDisplay {
|
||||
pub(crate) fn new(original: Body) -> Self {
|
||||
SharedDisplay { original: Body { display_of: None, meshes: None, ..original }, mesh: std::sync::OnceLock::new() }
|
||||
SharedDisplay { original: Body { display_of: None, meshes: None, ..original }, meshes: std::sync::Mutex::new(Vec::new()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +280,6 @@ pub struct FaceInfo {
|
||||
pub plane_normal: Option<Vec3>,
|
||||
}
|
||||
|
||||
type MeshedShell = truck_topology::Shell<mt::Point3, PolylineCurve<mt::Point3>, Option<PolygonMesh>>;
|
||||
|
||||
/// The most one facet of the display mesh turns (radians): a curved face or edge is divided
|
||||
/// until no facet spans more of it than this, however small its radius is beside the body.
|
||||
@@ -276,164 +295,56 @@ const DISPLAY_ANGLE_FLOOR: f64 = 1e-3;
|
||||
/// The finest angle a display mesh is divided by (one degree).
|
||||
const MIN_DISPLAY_ANGLE: f64 = std::f64::consts::PI / 180.0;
|
||||
|
||||
/// Triangulate each shell (not the solid: truck's solid constructor panics on the open or
|
||||
/// non-manifold shells an import can produce). Boundaries that lie on their surface only within
|
||||
/// tolerance (imports, boolean results) are projected onto it.
|
||||
fn mesh_shells(solid: &Solid, tol: f64) -> Vec<MeshedShell> {
|
||||
mesh_shells_within(solid, Deflection { display: true, ..Deflection::chord(tol) })
|
||||
/// How closely [`Body::mesh`] follows the exact surfaces.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct MeshTolerance {
|
||||
/// The farthest a facet or an edge segment may stand off the surface or curve it stands for
|
||||
/// (mm). Not finite or not positive: 0.05 mm. Never finer than a millionth of the body.
|
||||
pub chord: f64,
|
||||
/// The most a facet may turn (radians, at least one degree) where its sag is above a
|
||||
/// thousandth of `chord`; `None`: `chord` alone.
|
||||
pub angle: Option<f64>,
|
||||
}
|
||||
|
||||
/// [`mesh_shells`] divided by a chord tolerance and, when it has one, a bound on the angle a
|
||||
/// facet spans. Each edge is divided once for both its faces, so the faces still share its
|
||||
/// points.
|
||||
fn mesh_shells_within(solid: &Solid, deflection: Deflection) -> Vec<MeshedShell> {
|
||||
solid
|
||||
.boundaries()
|
||||
.iter()
|
||||
// The robust search tries the exact one first, so exact geometry costs nothing extra; a
|
||||
// face can also come back as an empty mesh rather than none (boolean results with
|
||||
// approximate intersection curves), so always use it.
|
||||
.map(|sh| sh.robust_triangulation_within(deflection))
|
||||
.collect()
|
||||
}
|
||||
impl MeshTolerance {
|
||||
/// `chord` alone.
|
||||
pub const fn chord(chord: f64) -> Self {
|
||||
MeshTolerance { chord, angle: None }
|
||||
}
|
||||
|
||||
/// A face's triangles (corner positions and normals, in the mesh's winding) with every edge
|
||||
/// whose middle sags more than `tol` from the surface split there, found through the mesh's
|
||||
/// surface parameters. A split depends on the edge alone, so neighbours stay conforming.
|
||||
/// `None` when nothing needs splitting or the parameters don't match the surface.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn refined(pm: &PolygonMesh, surf: &mt::Surface, tol: f64) -> Option<Vec<[(Vec3, Vec3); 3]>> {
|
||||
refined_with_cache(pm, surf, tol, true)
|
||||
}
|
||||
/// What the viewport draws: `chord`, and no facet turning more than [`DISPLAY_ANGLE`], so
|
||||
/// small radii on a large body look round too.
|
||||
pub const fn display(chord: f64) -> Self {
|
||||
MeshTolerance { chord, angle: Some(DISPLAY_ANGLE) }
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn refined_with_cache(pm: &PolygonMesh, surf: &mt::Surface, tol: f64, memoize: bool) -> Option<Vec<[(Vec3, Vec3); 3]>> {
|
||||
use mt::{ParametricSurface, ParametricSurface3D};
|
||||
let (pos, uvs, nor) = (pm.positions(), pm.uv_coords(), pm.normals());
|
||||
if uvs.is_empty() {
|
||||
return None;
|
||||
/// The application's display mesh of a body of size `size` (its box diagonal): 1/1000 of
|
||||
/// it, at least 1 µm, at [`DISPLAY_ANGLE`].
|
||||
pub fn display_of(size: f64) -> Self {
|
||||
MeshTolerance::display((size * 1e-3).max(1e-3))
|
||||
}
|
||||
type V = (Vec3, (f64, f64), Vec3);
|
||||
let mut tris: Vec<[V; 3]> = Vec::new();
|
||||
for tri in pm.faces().triangle_iter() {
|
||||
let mut t = [(Vec3::ZERO, (0.0, 0.0), Vec3::Z); 3];
|
||||
for (slot, v) in t.iter_mut().zip(tri.iter()) {
|
||||
let p = pos.get(v.pos)?;
|
||||
let uv = uvs.get(v.uv?)?;
|
||||
let n = v.nor.and_then(|i| nor.get(i)).map(|n| Vec3::new(n.x, n.y, n.z)).unwrap_or(Vec3::Z);
|
||||
*slot = (Vec3::new(p.x, p.y, p.z), (uv.x, uv.y), n);
|
||||
}
|
||||
tris.push(t);
|
||||
|
||||
/// For export to a mesh format (STL, 3MF, OBJ) of a body of size `size`: 1/5000 of it, at
|
||||
/// least 1 µm, the chord alone.
|
||||
pub fn export_of(size: f64) -> Self {
|
||||
MeshTolerance::chord((size * 2e-4).max(1e-3))
|
||||
}
|
||||
// The parameters must be the surface's own.
|
||||
let check = tris.iter().take(8).flat_map(|t| t.iter()).all(|(p, (u, v), _)| from_p3(surf.subs(*u, *v)).dist(*p) < tol * 4.0 + 1e-9);
|
||||
// Bounded work: a face may grow to a few times its triangles (wild surfaces sag
|
||||
// everywhere and are left as meshed).
|
||||
let cap = (tris.len() * 8).clamp(2_000, 60_000);
|
||||
if !check || tris.len() > 50_000 {
|
||||
return None;
|
||||
|
||||
/// The planner's chord tolerance and facet angle for a body of size `size`.
|
||||
pub(crate) fn rules(&self, size: f64) -> (f64, Option<crate::tess::size::Turn>) {
|
||||
let tol = if self.chord.is_finite() && self.chord > 0.0 { self.chord } else { 0.05 };
|
||||
let tol = tol.max(size * 1e-6);
|
||||
let turn = self
|
||||
.angle
|
||||
.filter(|a| a.is_finite() && *a > 0.0)
|
||||
.map(|a| crate::tess::size::Turn { angle: a.max(MIN_DISPLAY_ANGLE), floor: tol * DISPLAY_ANGLE_FLOOR });
|
||||
(tol, turn)
|
||||
}
|
||||
// Edges on the face's boundary are shared with the next face: never split them.
|
||||
let k = |x: &V| ((x.1.0 * 1e9).round() as i64, (x.1.1 * 1e9).round() as i64);
|
||||
let ek = |a: &V, b: &V| if k(a) < k(b) { (k(a), k(b)) } else { (k(b), k(a)) };
|
||||
// Only exact finite values identify reusable evaluations. In particular,
|
||||
// the rounded UV keys used for boundary incidence cannot identify a point
|
||||
// whose derivatives or midpoint may be reused.
|
||||
let vertex_key = |v: &V| {
|
||||
let values = [v.0.x, v.0.y, v.0.z, v.1.0, v.1.1, v.2.x, v.2.y, v.2.z];
|
||||
values.iter().all(|x| x.is_finite()).then(|| values.map(f64::to_bits))
|
||||
};
|
||||
let cache_limit = cap * 3;
|
||||
let mut singular_cache = std::collections::HashMap::<(u64, u64), bool>::new();
|
||||
let mut changed = false;
|
||||
for _ in 0..4 {
|
||||
let mut uses: std::collections::HashMap<((i64, i64), (i64, i64)), u32> = std::collections::HashMap::new();
|
||||
for [a, b, c] in &tris {
|
||||
for e in [ek(a, b), ek(b, c), ek(c, a)] {
|
||||
*uses.entry(e).or_default() += 1;
|
||||
}
|
||||
}
|
||||
let mut edge_cache = std::collections::HashMap::<([u64; 8], [u64; 8]), Option<V>>::new();
|
||||
let mut at = |a: &V, b: &V| -> Option<V> {
|
||||
let key = if memoize { vertex_key(a).zip(vertex_key(b)).map(|(a, b)| if a < b { (a, b) } else { (b, a) }) } else { None };
|
||||
if let Some(key) = key
|
||||
&& let Some(value) = edge_cache.get(&key)
|
||||
{
|
||||
return *value;
|
||||
}
|
||||
let mut calculate = || -> Option<V> {
|
||||
if uses.get(&ek(a, b)).copied().unwrap_or(0) < 2 {
|
||||
return None;
|
||||
}
|
||||
// Not at a degenerate point (a cone's apex: its parameters there are arbitrary).
|
||||
let mut singular = |x: &V| {
|
||||
let key = (x.1.0.to_bits(), x.1.1.to_bits());
|
||||
let cacheable = memoize && x.1.0.is_finite() && x.1.1.is_finite();
|
||||
if cacheable && let Some(value) = singular_cache.get(&key) {
|
||||
return *value;
|
||||
}
|
||||
let (du, dv) = (surf.uder(x.1.0, x.1.1), surf.vder(x.1.0, x.1.1));
|
||||
let c = du.cross(dv);
|
||||
let value = c.x * c.x + c.y * c.y + c.z * c.z
|
||||
<= 1e-12 * (du.x * du.x + du.y * du.y + du.z * du.z + dv.x * dv.x + dv.y * dv.y + dv.z * dv.z).powi(2) + 1e-300;
|
||||
if cacheable && singular_cache.len() < cache_limit {
|
||||
singular_cache.insert(key, value);
|
||||
}
|
||||
value
|
||||
};
|
||||
if singular(a) || singular(b) {
|
||||
return None;
|
||||
}
|
||||
let (u, v) = ((a.1.0 + b.1.0) / 2.0, (a.1.1 + b.1.1) / 2.0);
|
||||
let q = from_p3(surf.subs(u, v));
|
||||
// Within tolerance, or so far off that the parameters wrapped (a seam).
|
||||
let sag = q.dist((a.0 + b.0) * 0.5);
|
||||
if sag <= tol || sag >= 0.5 * a.0.dist(b.0) {
|
||||
return None;
|
||||
}
|
||||
let n = surf.normal(u, v);
|
||||
let mut n = Vec3::new(n.x, n.y, n.z);
|
||||
// Keep the mesh's normal side.
|
||||
if n.dot(a.2 + b.2) < 0.0 {
|
||||
n = -n;
|
||||
}
|
||||
Some((q, (u, v), n))
|
||||
};
|
||||
let value = calculate();
|
||||
if let Some(key) = key
|
||||
&& edge_cache.len() < cache_limit
|
||||
&& value.is_none_or(|v| vertex_key(&v).is_some())
|
||||
{
|
||||
edge_cache.insert(key, value);
|
||||
}
|
||||
value
|
||||
};
|
||||
let mut next = Vec::with_capacity(tris.len());
|
||||
let mut any = false;
|
||||
for t in &tris {
|
||||
let [a, b, c] = t;
|
||||
let m = [at(a, b), at(b, c), at(c, a)];
|
||||
any |= m.iter().any(Option::is_some);
|
||||
match m {
|
||||
[None, None, None] => next.push(*t),
|
||||
[Some(x), Some(y), Some(z)] => {
|
||||
next.extend([[*a, x, z], [x, *b, y], [z, y, *c], [x, y, z]]);
|
||||
}
|
||||
[Some(x), None, None] => next.extend([[*a, x, *c], [x, *b, *c]]),
|
||||
[None, Some(y), None] => next.extend([[*a, *b, y], [*a, y, *c]]),
|
||||
[None, None, Some(z)] => next.extend([[*a, *b, z], [z, *b, *c]]),
|
||||
[Some(x), Some(y), None] => next.extend([[*a, x, *c], [x, *b, y], [x, y, *c]]),
|
||||
[None, Some(y), Some(z)] => next.extend([[*a, *b, z], [z, *b, y], [z, y, *c]]),
|
||||
[Some(x), None, Some(z)] => next.extend([[*a, x, z], [x, *b, *c], [x, *c, z]]),
|
||||
}
|
||||
}
|
||||
tris = next;
|
||||
changed |= any;
|
||||
if !any || tris.len() > cap {
|
||||
break;
|
||||
}
|
||||
|
||||
/// The key a kept mesh is found by.
|
||||
fn key(&self) -> (u64, u64) {
|
||||
(self.chord.to_bits(), self.angle.map_or(u64::MAX, f64::to_bits))
|
||||
}
|
||||
changed.then(|| tris.into_iter().map(|t| t.map(|(p, _, n)| (p, n))).collect())
|
||||
}
|
||||
|
||||
/// Revolved surfaces are periodic in their angle; a face whose angles start at 0 is found at 0
|
||||
@@ -580,43 +491,17 @@ thread_local! {
|
||||
|
||||
/// The volume one closed shell encloses, signed by the way its faces face (positive when they
|
||||
/// face out of it), from a mesh as coarse as the orientation mesh of a solid of its size.
|
||||
/// `None` when it cannot be meshed.
|
||||
/// `None` when the planner cannot mesh it.
|
||||
pub(crate) fn shell_volume(shell: &mt::Shell) -> Option<f64> {
|
||||
let bounds: Aabb3 = shell.vertex_iter().map(|v| from_p3(v.point())).collect();
|
||||
let span = if bounds.is_empty() { 1.0 } else { bounds.diagonal() };
|
||||
let deflection = Deflection { display: true, ..Deflection::chord(orientation_tolerance(span)) };
|
||||
let v = guard("shell volume", || Ok(shell.robust_triangulation_within(deflection).to_polygon().volume())).ok()?;
|
||||
let solid = Solid::new_unchecked(vec![shell.clone()]);
|
||||
let v = coarse_mesh(&solid, orientation_tolerance(span_of(&solid))).ok()?.measure().volume;
|
||||
v.is_finite().then_some(v)
|
||||
}
|
||||
|
||||
/// A solid's volume off a mesh at `tol`, negative when it faces inwards, and how many faces that
|
||||
/// mesh left out.
|
||||
fn signed_volume(solid: &Solid, tol: f64) -> Result<(f64, usize)> {
|
||||
let orient = |tol: f64| {
|
||||
guard("orient", || {
|
||||
let shells = mesh_shells(solid, tol);
|
||||
let volume = shells.iter().map(|s| s.to_polygon().volume()).sum::<f64>();
|
||||
let missing =
|
||||
shells.iter().flat_map(|s| s.face_iter()).filter(|f| f.surface().is_none_or(|m| m.faces().triangle_iter().next().is_none())).count();
|
||||
Ok((volume, missing))
|
||||
})
|
||||
};
|
||||
let (mut v, missing) = orient(tol)?;
|
||||
// A long thin round part (a through hole's tool: a cylinder whose vertices all lie on
|
||||
// its seam, so they span its length only) meshed that coarsely can come out with a
|
||||
// negative volume when it is outward already: check finely before turning it inside out.
|
||||
let fine = (tol * 0.02).max(1e-4);
|
||||
if v < 0.0 && fine < tol {
|
||||
(v, _) = orient(fine)?;
|
||||
}
|
||||
Ok((v, missing))
|
||||
}
|
||||
|
||||
/// Whether a closed shell faces inwards (a void), by the check [`Body::new`] orients a solid by.
|
||||
/// Unlike [`shell_volume`], a negative coarse volume is checked again on a finer mesh.
|
||||
pub(crate) fn faces_inward(shell: &mt::Shell) -> Result<bool> {
|
||||
let solid = guard("shell", || Solid::try_new(vec![shell.clone()]).map_err(|e| KernelError::Failed(format!("shell: {e}"))))?;
|
||||
Ok(signed_volume(&solid, orientation_tolerance(span_of(&solid)))?.0 < 0.0)
|
||||
/// `solid` meshed by the planner at chord tolerance `tol` alone, before it is a [`Body`] (whose
|
||||
/// [`Body::mesh`] this is).
|
||||
fn coarse_mesh(solid: &Solid, tol: f64) -> Result<Mesh> {
|
||||
guard("orientation mesh", || crate::tess::plan(solid, tol, None).map(|(plan, _)| plan.evaluate()))
|
||||
}
|
||||
|
||||
/// Deflection the orientation mesh uses: coarse enough for a large solid, fine enough that a
|
||||
@@ -669,15 +554,21 @@ impl Body {
|
||||
return Err(KernelError::Failed("empty result".into()));
|
||||
}
|
||||
let mut solid = rephase_revolved(solid);
|
||||
// Orient outward: a closed solid must have positive volume (meshed finely enough that
|
||||
// every face comes out: a face missing from a coarse mesh can flip the sign).
|
||||
// Orient outward: a closed solid must have positive volume. Every face is meshed (a face
|
||||
// the planner cannot take is an error), so no missing face can flip the sign.
|
||||
let span = span_of(&solid);
|
||||
let tol = orientation_tolerance(span);
|
||||
#[cfg(test)]
|
||||
ORIENTED.with(|n| n.set(n.get() + 1));
|
||||
// The faces missing from the coarse mesh are the same whichever way the solid turns out
|
||||
// to face, so the count is kept for `unmeshed_faces` either way.
|
||||
let (v, missing) = signed_volume(&solid, tol)?;
|
||||
let mesh = coarse_mesh(&solid, tol)?;
|
||||
let v = mesh.measure().volume;
|
||||
let mut has = vec![false; solid.face_iter().count()];
|
||||
for f in &mesh.tri_face {
|
||||
if let Some(h) = has.get_mut(*f as usize) {
|
||||
*h = true;
|
||||
}
|
||||
}
|
||||
let missing = has.iter().filter(|h| !**h).count();
|
||||
if v < 0.0 {
|
||||
solid.not();
|
||||
}
|
||||
@@ -1226,7 +1117,7 @@ impl Body {
|
||||
{
|
||||
return Some(volume);
|
||||
}
|
||||
let volume = self.tessellate(f64::from_bits(key)).ok()?.measure().volume;
|
||||
let volume = self.mesh(&crate::MeshTolerance::chord(f64::from_bits(key))).ok()?.measure().volume;
|
||||
if let Some(cell) = &self.volume
|
||||
&& let Ok(mut slot) = cell.lock()
|
||||
{
|
||||
@@ -1277,34 +1168,52 @@ impl Body {
|
||||
self
|
||||
}
|
||||
|
||||
/// Triangulate with chord tolerance `tol` (mm). Triangles carry their face index.
|
||||
pub fn tessellate(&self, tol: f64) -> Result<Mesh> {
|
||||
self.tessellate_with(tol, false)
|
||||
/// The body's triangles, `t` from its exact surfaces: SolidCraft's one mesher (the planner,
|
||||
/// [`crate::tess`]), evaluated on the CPU, with its edges flagged for display (seams, tangent
|
||||
/// joins, protected creases). Triangles carry their face index and the edges their polylines,
|
||||
/// in [`Body::edges`] order; neighbouring faces share every boundary node, so the mesh is
|
||||
/// watertight wherever the B-rep is. A mesh body gives its own triangles. A face the planner
|
||||
/// cannot mesh is an error that names it and why.
|
||||
pub fn mesh(&self, t: &MeshTolerance) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.mesh_untimed(t))
|
||||
}
|
||||
|
||||
/// Like [`Body::tessellate`]; with `refine`, triangles that cut through curved faces are
|
||||
/// split until within `tol` of the surface (for measuring).
|
||||
pub fn tessellate_with(&self, tol: f64, refine: bool) -> Result<Mesh> {
|
||||
self.tessellate_within(tol, f64::INFINITY, refine)
|
||||
fn mesh_untimed(&self, t: &MeshTolerance) -> Result<Mesh> {
|
||||
if let Some(m) = &self.mesh {
|
||||
return Ok(m.to_mesh());
|
||||
}
|
||||
// A placed copy shows its original's mesh, moved: the part is meshed once for every copy.
|
||||
if let Some((shared, m)) = self.display_of.as_deref() {
|
||||
let made = remembered(&shared.meshes, t, || shared.original.mesh_untimed(t))?;
|
||||
if let Some(placed) = placed_mesh(&made, m) {
|
||||
return Ok(placed);
|
||||
}
|
||||
}
|
||||
match self.meshes.as_ref().filter(|m| std::sync::Arc::ptr_eq(&m.solid, &self.solid)) {
|
||||
Some(memo) => Ok(Mesh::clone(&remembered(&memo.made, t, || self.mesh_of(&self.mesh_plan_untimed(t)?))?)),
|
||||
None => self.mesh_of(&self.mesh_plan_untimed(t)?),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Body::tessellate_with`] in which no facet turns more than `angle` radians either
|
||||
/// (infinite: the chord tolerance alone).
|
||||
fn tessellate_within(&self, tol: f64, angle: f64, refine: bool) -> Result<Mesh> {
|
||||
let mesh = || crate::profile::timed(crate::profile::Kind::Tessellate, || self.tessellate_within_untimed(tol, angle, refine));
|
||||
let memo = self.meshes.as_ref().filter(|m| std::sync::Arc::ptr_eq(&m.solid, &self.solid));
|
||||
// Held while meshing: threads asking for the same mesh wait for it rather than make it too.
|
||||
let Some(Ok(mut made)) = memo.map(|m| m.made.lock()) else { return mesh() };
|
||||
let key = (tol.to_bits(), angle.to_bits(), refine);
|
||||
if let Some((_, m)) = made.iter().find(|(k, _)| *k == key) {
|
||||
return Ok(Mesh::clone(m));
|
||||
/// The plan [`Body::mesh`] evaluates: every face's triangulation in its own parameters, each
|
||||
/// edge sampled once so faces meet exactly. For an evaluator of its own (the GPU viewport);
|
||||
/// [`Body::mesh_of`] gives its mesh without planning again.
|
||||
pub fn mesh_plan(&self, t: &MeshTolerance) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.mesh_plan_untimed(t))
|
||||
}
|
||||
|
||||
fn mesh_plan_untimed(&self, t: &MeshTolerance) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
if self.is_mesh() {
|
||||
return Err(KernelError::Invalid("a mesh body has no surfaces to tessellate".into()));
|
||||
}
|
||||
let m = mesh()?;
|
||||
if made.len() >= MEMO_MESHES {
|
||||
made.remove(0);
|
||||
}
|
||||
made.push((key, std::sync::Arc::new(m.clone())));
|
||||
Ok(m)
|
||||
let (tol, turn) = t.rules(self.size());
|
||||
guard("tessellate", || crate::tess::plan(&self.solid, tol, turn).map(|(plan, _)| plan))
|
||||
}
|
||||
|
||||
/// The mesh of `plan`, a plan [`Body::mesh_plan`] made of this body: what [`Body::mesh`]
|
||||
/// gives at the plan's tolerance.
|
||||
pub fn mesh_of(&self, plan: &solidcraft_geom::tess::TessPlan) -> Result<Mesh> {
|
||||
self.flagged(plan.evaluate())
|
||||
}
|
||||
|
||||
/// A copy of this body that keeps the meshes made of it for reuse, for as long as the copy
|
||||
@@ -1332,136 +1241,6 @@ impl Body {
|
||||
Some(made)
|
||||
}
|
||||
|
||||
fn tessellate_within_untimed(&self, tol: f64, angle: f64, refine: bool) -> Result<Mesh> {
|
||||
let tol = if tol.is_finite() && tol > 0.0 { tol } else { 0.05 };
|
||||
let tol = tol.max(self.size() * 1e-6);
|
||||
if let Some(m) = &self.mesh {
|
||||
return Ok(m.to_mesh());
|
||||
}
|
||||
// A face trimmed by a helix (a modelled thread from another CAD system) turns more than
|
||||
// once around its cylinder; truck meshes it with triangles across the solid (#1289).
|
||||
// Such a body is meshed from its display plan, which cuts the face along a seam.
|
||||
// So is one bounded by a single seam from pole to pole (a sphere from SMLib), which truck
|
||||
// meshes as nothing.
|
||||
if guard("tessellate", || Ok(crate::tess::seam::turns_more_than_once(&self.solid) || crate::tess::has_pole_to_pole_seam(&self.solid)))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let turn = (angle.is_finite() && angle > 0.0)
|
||||
.then(|| crate::tess::size::Turn { angle: angle.max(MIN_DISPLAY_ANGLE), floor: tol * DISPLAY_ANGLE_FLOOR });
|
||||
match guard("tessellate", || crate::tess::plan(&self.solid, tol, turn, self.boundary_tol)) {
|
||||
Ok((plan, _)) => return Ok(plan.evaluate()),
|
||||
Err(e) => log::debug!("no display plan ({e}); meshing the reference way"),
|
||||
}
|
||||
}
|
||||
let deflection = if angle.is_finite() && angle > 0.0 {
|
||||
Deflection { chord: tol, angle: angle.max(MIN_DISPLAY_ANGLE), floor: tol * DISPLAY_ANGLE_FLOOR, display: true }
|
||||
} else {
|
||||
Deflection { display: true, ..Deflection::chord(tol) }
|
||||
};
|
||||
guard("tessellate", || {
|
||||
let meshed = mesh_shells_within(&self.solid, deflection);
|
||||
let mut out = Mesh::default();
|
||||
let solid_faces: Vec<&mt::Face> = self.solid.face_iter().collect();
|
||||
for (fi, face) in meshed.iter().flat_map(|s| s.face_iter()).enumerate() {
|
||||
let Some(pm) = face.surface() else { continue };
|
||||
let flip = !face.orientation();
|
||||
let base = u32::try_from(out.positions.len()).unwrap_or(u32::MAX);
|
||||
// Curved faces: split triangles that cut through the surface.
|
||||
let refinement = if refine {
|
||||
solid_faces.get(fi).and_then(|sf| {
|
||||
let surface = sf.surface();
|
||||
if matches!(surface, mt::Surface::Plane(_)) { None } else { refined(&pm, &surface, tol) }
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(tris) = refinement {
|
||||
let fid = u32::try_from(fi).unwrap_or(u32::MAX);
|
||||
for t in tris {
|
||||
let mut idx = [0u32; 3];
|
||||
for (slot, (p, n)) in idx.iter_mut().zip(t) {
|
||||
out.positions.push(p);
|
||||
out.normals.push(if flip { -n } else { n });
|
||||
*slot = u32::try_from(out.positions.len() - 1).unwrap_or(0);
|
||||
}
|
||||
out.triangles.push(if flip { [idx[0], idx[2], idx[1]] } else { idx });
|
||||
out.tri_face.push(fid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let pos = pm.positions();
|
||||
let nor = pm.normals();
|
||||
// Expand to per-corner vertices so normals stay per face (crisp edges).
|
||||
for tri in pm.faces().triangle_iter() {
|
||||
let mut idx = [0u32; 3];
|
||||
for (k, v) in tri.iter().enumerate() {
|
||||
let Some(p) = pos.get(v.pos) else { continue };
|
||||
let n = v.nor.and_then(|ni| nor.get(ni)).map(|n| Vec3::new(n.x, n.y, n.z)).unwrap_or(Vec3::Z);
|
||||
let n = if flip { -n } else { n };
|
||||
out.positions.push(Vec3::new(p.x, p.y, p.z));
|
||||
out.normals.push(n);
|
||||
if let Some(slot) = idx.get_mut(k) {
|
||||
*slot = u32::try_from(out.positions.len() - 1).unwrap_or(0);
|
||||
}
|
||||
}
|
||||
let t = if flip { [idx[0], idx[2], idx[1]] } else { idx };
|
||||
out.triangles.push(t);
|
||||
out.tri_face.push(u32::try_from(fi).unwrap_or(u32::MAX));
|
||||
}
|
||||
let _ = base;
|
||||
}
|
||||
// Fill missing normals from geometry.
|
||||
for t in &out.triangles {
|
||||
let (Some(a), Some(b), Some(c)) =
|
||||
(out.positions.get(t[0] as usize), out.positions.get(t[1] as usize), out.positions.get(t[2] as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let fnrm = (b - a).cross(c - a).normalized().unwrap_or(Vec3::Z);
|
||||
for k in t {
|
||||
if let Some(n) = out.normals.get_mut(*k as usize)
|
||||
&& (n.dot(fnrm) < -0.2 || n.len() < 0.5)
|
||||
{
|
||||
*n = fnrm;
|
||||
}
|
||||
}
|
||||
}
|
||||
let (edges, faces) = edge_order(meshed.iter().flat_map(|s| s.face_iter()));
|
||||
out.edges = edges.iter().map(|e| e.oriented_curve().0.iter().map(|p| Vec3::new(p.x, p.y, p.z)).collect()).collect();
|
||||
out.edge_faces = faces.into_iter().map(|fs| fs.into_iter().map(|f| u32::try_from(f).unwrap_or(u32::MAX)).collect()).collect();
|
||||
Ok(out)
|
||||
})
|
||||
}
|
||||
|
||||
/// The display tessellation as a plan for the CPU or GPU evaluator: every face's
|
||||
/// triangulation in its own parameters, with each edge sampled once (chord tolerance
|
||||
/// `tol`, mm) so faces meet exactly.
|
||||
pub fn display_plan(&self, tol: f64) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
self.plan_within(tol, None)
|
||||
}
|
||||
|
||||
/// Like [`Body::display_plan`], and no facet turns more than `angle` radians (see
|
||||
/// [`Body::display_mesh_angular`]).
|
||||
pub fn display_plan_angular(&self, tol: f64, angle: f64) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
self.plan_within(tol, Some(angle))
|
||||
}
|
||||
|
||||
fn plan_within(&self, tol: f64, angle: Option<f64>) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.plan_within_untimed(tol, angle))
|
||||
}
|
||||
|
||||
fn plan_within_untimed(&self, tol: f64, angle: Option<f64>) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
if self.is_mesh() {
|
||||
return Err(KernelError::Invalid("a mesh body has no surfaces to tessellate".into()));
|
||||
}
|
||||
let tol = if tol.is_finite() && tol > 0.0 { tol } else { 0.05 };
|
||||
let tol = tol.max(self.size() * 1e-6);
|
||||
// The same angle and floor as the reference mesher's angular display mesh.
|
||||
let turn =
|
||||
angle.filter(|a| a.is_finite()).map(|a| crate::tess::size::Turn { angle: a.max(MIN_DISPLAY_ANGLE), floor: tol * DISPLAY_ANGLE_FLOOR });
|
||||
guard("tessellate", || crate::tess::plan(&self.solid, tol, turn, self.boundary_tol).map(|(plan, _)| plan))
|
||||
}
|
||||
|
||||
/// Planar faces whose boundary crosses itself: two of its loops crossing each other, or a
|
||||
/// loop crossing itself. Such a face has no inside, and its mesh covers what it shouldn't
|
||||
/// and leaves openings beside it, while the shell is still closed and every face meshes
|
||||
@@ -1589,78 +1368,6 @@ impl Body {
|
||||
Some(has.iter().filter(|h| !**h).count())
|
||||
}
|
||||
|
||||
/// Display mesh: like [`Body::tessellate`], with seam edges flagged. Meshed from the
|
||||
/// body's [`Body::display_plan`] (shared edge nodes, structured grids), or the way
|
||||
/// [`Body::tessellate`] meshes when no plan can be made.
|
||||
pub fn display_mesh(&self, tol: f64) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.display_mesh_untimed(tol))
|
||||
}
|
||||
|
||||
fn display_mesh_untimed(&self, tol: f64) -> Result<Mesh> {
|
||||
if self.is_mesh() {
|
||||
return self.flagged(self.tessellate(tol)?);
|
||||
}
|
||||
let m = match self.display_plan(tol) {
|
||||
Ok(plan) => plan.evaluate(),
|
||||
Err(e) => {
|
||||
log::debug!("display mesh: {e}; meshing the reference way");
|
||||
self.tessellate(tol)?
|
||||
}
|
||||
};
|
||||
self.flagged(m)
|
||||
}
|
||||
|
||||
/// Display mesh whose curved faces and edges look curved whatever the body's size: like
|
||||
/// [`Body::display_mesh`], and no facet turns more than `angle` radians (see
|
||||
/// [`DISPLAY_ANGLE`]). A chord tolerance that follows the body's size is larger than the
|
||||
/// sagitta of a small radius on a large body (a sheet-metal bend, a small hole), which the
|
||||
/// tolerance alone leaves as one flat facet.
|
||||
pub fn display_mesh_angular(&self, tol: f64, angle: f64) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.display_mesh_angular_untimed(tol, angle))
|
||||
}
|
||||
|
||||
fn display_mesh_angular_untimed(&self, tol: f64, angle: f64) -> Result<Mesh> {
|
||||
if self.is_mesh() {
|
||||
return self.flagged(self.tessellate(tol)?);
|
||||
}
|
||||
match self.display_plan_angular(tol, angle) {
|
||||
Ok(plan) => self.display_mesh_of(&plan),
|
||||
Err(e) => {
|
||||
log::debug!("display mesh: {e}; meshing the reference way");
|
||||
self.reference_display_mesh_angular(tol, angle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The display mesh of a plan made from this body ([`Body::display_plan`],
|
||||
/// [`Body::display_plan_angular`]): evaluated on the CPU, with its edges flagged.
|
||||
pub fn display_mesh_of(&self, plan: &solidcraft_geom::tess::TessPlan) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.display_mesh_of_untimed(plan))
|
||||
}
|
||||
|
||||
fn display_mesh_of_untimed(&self, plan: &solidcraft_geom::tess::TessPlan) -> Result<Mesh> {
|
||||
self.flagged(plan.evaluate())
|
||||
}
|
||||
|
||||
/// The reference mesher's angular display mesh: what [`Body::display_mesh_angular`] falls
|
||||
/// back to when no plan can be made.
|
||||
pub fn reference_display_mesh_angular(&self, tol: f64, angle: f64) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.reference_display_mesh_angular_untimed(tol, angle))
|
||||
}
|
||||
|
||||
fn reference_display_mesh_angular_untimed(&self, tol: f64, angle: f64) -> Result<Mesh> {
|
||||
if let Some((shared, t)) = self.display_of.as_deref()
|
||||
&& let Some(placed) =
|
||||
shared.mesh.get_or_init(|| shared.original.reference_display_mesh_angular(tol, angle).ok()).as_ref().and_then(|m| placed_mesh(m, t))
|
||||
{
|
||||
return Ok(placed);
|
||||
}
|
||||
if self.is_mesh() {
|
||||
return self.flagged(self.tessellate(tol)?);
|
||||
}
|
||||
self.flagged(self.tessellate_within(tol, angle, false)?)
|
||||
}
|
||||
|
||||
/// A tessellation with its edges flagged for display (seams, tangent joins, protected creases).
|
||||
fn flagged(&self, mut m: Mesh) -> Result<Mesh> {
|
||||
if self.is_mesh() {
|
||||
@@ -1673,7 +1380,7 @@ impl Body {
|
||||
|
||||
/// Edges with polylines (index = position in [`Body::tessellate`]'s `edges`).
|
||||
pub fn edges(&self, tol: f64) -> Result<Vec<EdgeInfo>> {
|
||||
let m = self.tessellate(tol)?;
|
||||
let m = self.mesh(&crate::MeshTolerance::chord(tol))?;
|
||||
Ok(m.edges
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -1709,7 +1416,7 @@ impl Body {
|
||||
|
||||
/// Faces with area and centroid.
|
||||
pub fn faces(&self, tol: f64) -> Result<Vec<FaceInfo>> {
|
||||
let m = self.tessellate(tol)?;
|
||||
let m = self.mesh(&crate::MeshTolerance::chord(tol))?;
|
||||
let nf = self.face_count();
|
||||
let mut acc: Vec<(f64, Vec3, Vec3, bool)> = vec![(0.0, Vec3::ZERO, Vec3::ZERO, true); nf];
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
@@ -1759,7 +1466,7 @@ impl Body {
|
||||
|
||||
/// Is the point inside the solid?
|
||||
pub fn contains(&self, p: Vec3) -> bool {
|
||||
self.tessellate(self.size() * 2e-3).map(|m| m.contains(p)).unwrap_or(false)
|
||||
self.mesh(&crate::MeshTolerance::chord(self.size() * 2e-3)).map(|m| m.contains(p)).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// B-rep faces (a mesh body is one face; [`crate::measure`] counts its triangles).
|
||||
@@ -1836,7 +1543,7 @@ impl Body {
|
||||
self.require_brep("contact test")?;
|
||||
other.require_brep("contact test")?;
|
||||
let tol = (self.size().max(other.size()) * 1e-4).max(1e-6);
|
||||
let (a, b) = (self.tessellate(tol)?, other.tessellate(tol)?);
|
||||
let (a, b) = (self.mesh(&crate::MeshTolerance::chord(tol))?, other.mesh(&crate::MeshTolerance::chord(tol))?);
|
||||
shell_groups::meshes_meet(&a, &b, 2.0 * tol)
|
||||
}
|
||||
|
||||
@@ -1859,10 +1566,10 @@ impl Body {
|
||||
if meshes.len() == 16 {
|
||||
meshes.clear();
|
||||
}
|
||||
meshes.insert(key, self.tessellate(tol)?);
|
||||
meshes.insert(key, self.mesh(&crate::MeshTolerance::chord(tol))?);
|
||||
}
|
||||
let mesh = meshes.get(&key).ok_or_else(|| KernelError::Internal("contact mesh disappeared".into()))?;
|
||||
contacts.push(shell_groups::meshes_meet(mesh, &other.tessellate(tol)?, 2.0 * tol)?);
|
||||
contacts.push(shell_groups::meshes_meet(mesh, &other.mesh(&crate::MeshTolerance::chord(tol))?, 2.0 * tol)?);
|
||||
}
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ fn nested_island() -> Body {
|
||||
|
||||
fn assert_material(body: &Body, expected: f64, probes: &[(Vec3, bool)]) {
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
let mesh = body.tessellate_with(0.01, true).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
assert!((mesh.measure().volume - expected).abs() < expected * 1e-6, "{} vs {expected}", mesh.measure().volume);
|
||||
for (p, want) in probes {
|
||||
assert_eq!(mesh.contains(*p), *want, "material at {p:?}");
|
||||
@@ -31,7 +31,7 @@ fn check_nested(body: &Body) {
|
||||
for (got, want) in volumes.iter().zip([56_000.0, 488_000.0]) {
|
||||
assert!((got - want).abs() < 1e-6, "{volumes:?}");
|
||||
}
|
||||
let meshes: Vec<_> = lumps.iter().map(|b| b.tessellate_with(0.01, true).unwrap()).collect();
|
||||
let meshes: Vec<_> = lumps.iter().map(|b| b.mesh(&crate::MeshTolerance::chord(0.01)).unwrap()).collect();
|
||||
for (p, want) in probes {
|
||||
assert_eq!(meshes.iter().filter(|m| m.contains(p)).count(), usize::from(want), "lump material at {p:?}");
|
||||
}
|
||||
@@ -54,7 +54,7 @@ fn sphere_cut_far_from_a_void_keeps_the_void() {
|
||||
let got = measure(&cut).unwrap().volume;
|
||||
assert!((got - want).abs() < want * 1e-3, "volume {got}, want {want}");
|
||||
assert_eq!(cut.solid.boundaries().len(), 2, "the cavity stays a sealed second shell");
|
||||
let mesh = cut.tessellate_with(0.01, true).unwrap();
|
||||
let mesh = cut.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
for (p, material) in [(Vec3::new(27.5, 27.5, 27.5), false), (Vec3::new(1.0, 1.0, 39.0), false), (Vec3::new(20.0, 20.0, 20.0), true)] {
|
||||
assert_eq!(mesh.contains(p), material, "material at {p:?}");
|
||||
}
|
||||
@@ -108,7 +108,7 @@ fn nested_voids_survive_step_round_trip_and_further_cuts() {
|
||||
for lump in &lumps {
|
||||
if let Some(cut) = boolean(lump, &opening, BoolOp::Cut).unwrap() {
|
||||
cut_volume += measure(&cut).unwrap().volume;
|
||||
assert!(!cut.tessellate_with(0.01, true).unwrap().contains(Vec3::X * 15.0));
|
||||
assert!(!cut.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().contains(Vec3::X * 15.0));
|
||||
}
|
||||
}
|
||||
// Only x=10..20 is material along this 4×4 opening: 160 mm³ removed.
|
||||
@@ -132,7 +132,7 @@ fn curved_internal_void_and_disconnected_material_keep_analytic_volume() {
|
||||
for (got, want) in volumes.iter().zip(want) {
|
||||
assert!((got - want).abs() < want * 0.002, "{got} vs {want}");
|
||||
}
|
||||
let meshes: Vec<_> = lumps.iter().map(|b| b.tessellate_with(0.01, true).unwrap()).collect();
|
||||
let meshes: Vec<_> = lumps.iter().map(|b| b.mesh(&crate::MeshTolerance::chord(0.01)).unwrap()).collect();
|
||||
for (p, want) in [(Vec3::ZERO, 0), (Vec3::X * 3.0, 1), (Vec3::X * 7.0, 0), (Vec3::X * 15.0, 1)] {
|
||||
assert_eq!(meshes.iter().filter(|m| m.contains(p)).count(), want);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ fn curved_internal_void_and_disconnected_material_keep_analytic_volume() {
|
||||
// two bodies.
|
||||
let imported: Vec<_> = imported.bodies.into_iter().map(|b| b.body).collect();
|
||||
assert_eq!(imported.len(), 2);
|
||||
let imported_meshes: Vec<_> = imported.iter().map(|b| b.tessellate_with(0.01, true).unwrap()).collect();
|
||||
let imported_meshes: Vec<_> = imported.iter().map(|b| b.mesh(&crate::MeshTolerance::chord(0.01)).unwrap()).collect();
|
||||
for (p, want) in [(Vec3::ZERO, 0), (Vec3::X * 3.0, 1), (Vec3::X * 7.0, 0), (Vec3::X * 15.0, 1)] {
|
||||
assert_eq!(imported_meshes.iter().filter(|m| m.contains(p)).count(), want);
|
||||
}
|
||||
@@ -177,7 +177,7 @@ fn unowned_overlapping_and_touching_cavities_are_errors_not_dropped_geometry() {
|
||||
|
||||
#[test]
|
||||
fn shell_grouping_preserves_large_disconnected_patterns_and_bounds_hostile_work() {
|
||||
let mesh = cube(1.0).tessellate_with(0.01, true).unwrap();
|
||||
let mesh = cube(1.0).mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
let copies: Vec<_> = (0..512)
|
||||
.map(|i| {
|
||||
let mut m = mesh.clone();
|
||||
@@ -202,7 +202,7 @@ fn shell_grouping_preserves_large_disconnected_patterns_and_bounds_hostile_work(
|
||||
};
|
||||
let (repeated, lifted) = (slanted(0.0), slanted(1e-3));
|
||||
assert!(shell_groups::groups(&[repeated, lifted], &[8.0, 8.0], 1e-8).unwrap_err().to_string().contains("boundary work"));
|
||||
let mut outer = cube(2.0).tessellate_with(0.01, true).unwrap();
|
||||
let mut outer = cube(2.0).mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
let mut inner = mesh;
|
||||
outer.triangles = outer.triangles.iter().copied().cycle().take(4000).collect();
|
||||
inner.triangles = inner.triangles.iter().copied().cycle().take(4000).collect();
|
||||
@@ -222,9 +222,9 @@ fn translated_and_scaled_nested_voids_keep_material_membership() {
|
||||
.unwrap();
|
||||
let lumps = transformed
|
||||
.lumps()
|
||||
.unwrap_or_else(|e| panic!("scale={scale}: {e}; bounds={:?}", transformed.tessellate_with(1e-4, true).unwrap().bounds()));
|
||||
.unwrap_or_else(|e| panic!("scale={scale}: {e}; bounds={:?}", transformed.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().bounds()));
|
||||
assert_eq!(lumps.len(), 2);
|
||||
let meshes: Vec<_> = lumps.iter().map(|b| b.tessellate_with((scale * 0.01).max(1e-5), true).unwrap()).collect();
|
||||
let meshes: Vec<_> = lumps.iter().map(|b| b.mesh(&crate::MeshTolerance::chord((scale * 0.01).max(1e-5))).unwrap()).collect();
|
||||
for (x, want) in [(0.0, 0), (15.0, 1), (30.0, 0), (45.0, 1)] {
|
||||
assert_eq!(meshes.iter().filter(|m| m.contains(origin + Vec3::X * (x * scale))).count(), want);
|
||||
}
|
||||
@@ -311,7 +311,7 @@ fn grouping_lumps_preserves_only_unchanged_face_certificates_and_trimmed_caps()
|
||||
}
|
||||
total_faces += 1;
|
||||
}
|
||||
let mesh = lump.tessellate(1e-5).unwrap();
|
||||
let mesh = lump.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
if mesh.contains(Vec3::new(21., 21., 0.5)) {
|
||||
assert!(!mesh.contains(Vec3::new(25., 25., 0.5)), "trimmed stop hole must not refill");
|
||||
assert!((mesh.measure().volume - (100. - 4. * std::f64::consts::PI)).abs() < 0.002);
|
||||
@@ -343,7 +343,7 @@ fn a_long_thin_cylinder_comes_out_outward() {
|
||||
for (lo, seam) in [(-181.0, 0.613), (-100.0, 0.613), (-10.0, 0.613), (-181.0, 0.0)] {
|
||||
let disc = Region2 { outer: Loop2::circle_from(Vec2::ZERO, 0.5, seam), holes: vec![] };
|
||||
let tool = crate::extrude(&plane, &[disc], lo, 0.9).unwrap().pop().unwrap();
|
||||
let volume = tool.tessellate(0.01).unwrap().measure().volume;
|
||||
let volume = tool.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().volume;
|
||||
let want = std::f64::consts::PI * 0.25 * (0.9 - lo);
|
||||
assert!((volume - want).abs() < want * 0.02, "{lo} {seam}: {volume} vs {want}");
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ pub(crate) fn check_retry(a: &Body, b: &Body, op: BoolOp, result: Option<&Body>)
|
||||
// Nothing left: only a cut whose tool covers the whole target (to within the meshes'
|
||||
// chords: a tool touching a round side of the target).
|
||||
let chord = a.size().max(b.size()) * 1e-3;
|
||||
let (Ok(ma), Ok(mb)) = (a.tessellate(chord), b.tessellate(chord)) else { return Ok(()) };
|
||||
let (Ok(ma), Ok(mb)) = (a.mesh(&crate::MeshTolerance::chord(chord)), b.mesh(&crate::MeshTolerance::chord(chord))) else { return Ok(()) };
|
||||
let (ba, bb) = (ma.bounds(), mb.bounds());
|
||||
let tol = 2.0 * chord;
|
||||
let within = ba.min.x >= bb.min.x - tol
|
||||
@@ -158,7 +158,7 @@ pub(crate) fn check_retry(a: &Body, b: &Body, op: BoolOp, result: Option<&Body>)
|
||||
}
|
||||
let size = a.size().max(b.size());
|
||||
let t = (size * 2e-4).max(1e-5);
|
||||
let (Ok(ma), Ok(mb), Ok(mr)) = (a.tessellate_with(t, true), b.tessellate_with(t, true), r.tessellate_with(t, true)) else {
|
||||
let (Ok(ma), Ok(mb), Ok(mr)) = (a.mesh(&crate::MeshTolerance::chord(t)), b.mesh(&crate::MeshTolerance::chord(t)), r.mesh(&crate::MeshTolerance::chord(t))) else {
|
||||
return Err(bad(op, "the result could not be checked".into()));
|
||||
};
|
||||
// A mesh as coarse as `t` on a round smaller than it has no inside to speak of (a through
|
||||
@@ -171,7 +171,7 @@ pub(crate) fn check_retry(a: &Body, b: &Body, op: BoolOp, result: Option<&Body>)
|
||||
if fine == t && scale == 1.0 {
|
||||
return Ok(mesh.measure().volume);
|
||||
}
|
||||
body.tessellate_with(fine * scale, true).map(|m| m.measure().volume).map_err(|_| bad(op, "the result could not be checked".into()))
|
||||
body.mesh(&crate::MeshTolerance::chord(fine * scale)).map(|m| m.measure().volume).map_err(|_| bad(op, "the result could not be checked".into()))
|
||||
};
|
||||
let judge = |scale: f64| -> Result<(bool, f64, f64, f64)> {
|
||||
let (va, vb, vr) = (volume(a, &ma, scale)?, volume(b, &mb, scale)?, volume(r, &mr, scale)?);
|
||||
|
||||
@@ -90,8 +90,8 @@ fn verified<'a>(r: &'a Body, operands: &[&Body], tol: f64) -> &'a History {
|
||||
pub(crate) fn checked<'a>(r: &'a Body, operands: &[&Body], tol: f64, at: Reach, generated: &dyn Fn(usize) -> bool) -> &'a History {
|
||||
let h = r.history().expect("the operation reports its history");
|
||||
h.check(r, operands).unwrap();
|
||||
let rm = r.tessellate(tol).unwrap();
|
||||
let ms: Vec<Mesh> = operands.iter().map(|b| b.tessellate(tol).unwrap()).collect();
|
||||
let rm = r.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
let ms: Vec<Mesh> = operands.iter().map(|b| b.mesh(&crate::MeshTolerance::chord(tol)).unwrap()).collect();
|
||||
let near = 4.0 * tol + r.size() * 1e-6;
|
||||
let on = |o: &FaceOrigin, p: Vec3| match *o {
|
||||
FaceOrigin::Face { operand, face } => to_face(&ms[operand], face, p) < near.max(at.moved),
|
||||
@@ -414,7 +414,7 @@ fn nudged_tool_composes() {
|
||||
#[test]
|
||||
fn faceted_tool() {
|
||||
let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
|
||||
let tool = boxed(5.0, 5.0, 5.0, 15.0, 15.0, 15.0).tessellate(0.1).unwrap();
|
||||
let tool = boxed(5.0, 5.0, 5.0, 15.0, 15.0, 15.0).mesh(&crate::MeshTolerance::chord(0.1)).unwrap();
|
||||
let r = crate::faceted_boolean(&a, &tool, BoolOp::Cut, 0.01).unwrap().unwrap();
|
||||
let h = r.history().expect("the faceted boolean reports its history");
|
||||
h.check_counts((r.face_count(), r.edge_count()), &[(a.face_count(), a.edge_count()), (tool.triangles.len(), 0)]).unwrap();
|
||||
|
||||
@@ -24,8 +24,8 @@ fn fixture() -> (Body, Body, Vec3, f64) {
|
||||
let region = Region2 { outer: Loop2::circle(Vec2::new(20., 15.), 8.), holes: vec![Loop2::circle(Vec2::new(20., 15.), 5.).reversed()] };
|
||||
let ring = extrude(&Plane::XY, &[region], 14., 24.).unwrap().remove(0);
|
||||
let web_volume = 0.02 * 2. * (24f64.sqrt() + 25. * 0.2f64.asin());
|
||||
let fine_target = target.tessellate_with(1e-4, true).unwrap();
|
||||
let added = fine_target.measure().volume - bored.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let fine_target = target.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap();
|
||||
let added = fine_target.measure().volume - bored.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((added - web_volume).abs() < 0.005, "actual connected web adds {added}, expected {web_volume}");
|
||||
for x in [16., 20., 24.] {
|
||||
for y in [14.2, 15., 15.8] {
|
||||
@@ -45,15 +45,15 @@ fn thin_connected_web_between_bore_stations_survives_union() {
|
||||
let ring_before = step_export(&[&ring], "ring").unwrap();
|
||||
let size = target.size().max(ring.size());
|
||||
let tol = (size * 5e-4).max(size * 1e-5);
|
||||
let ma = target.tessellate_with(tol, true).unwrap();
|
||||
let mb = ring.tessellate_with(tol, true).unwrap();
|
||||
let ma = target.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
let mb = ring.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
assert!(ma.contains(probe), "fixture must contain the real connected web");
|
||||
assert!(!mb.contains(probe));
|
||||
let va = ma.measure().volume;
|
||||
let vb = mb.measure().volume;
|
||||
let slack = 2e-3 * (va + vb) + 1e-9;
|
||||
let accept = |body: &Body| {
|
||||
let mr = body.tessellate_with(tol, true).unwrap();
|
||||
let mr = body.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
let v = mr.measure().volume;
|
||||
let mismatch = super::mismatch(&ma, &mb, &mr, BoolOp::Union);
|
||||
eprintln!("fallback candidate V={v}, expected={expected}, web={}, mismatch={mismatch:?}", mr.contains(probe));
|
||||
@@ -73,10 +73,10 @@ fn thin_connected_web_between_bore_stations_survives_union() {
|
||||
std::fs::write(dir.join("public-union.step"), step_export(&[result], "public union").unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
let direct_keeps_web = direct.as_ref().is_none_or(|result| result.tessellate_with(1e-3, true).unwrap().contains(probe));
|
||||
let direct_keeps_web = direct.as_ref().is_none_or(|result| result.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().contains(probe));
|
||||
let public_keeps_web = match &public {
|
||||
Ok(Some(result)) => {
|
||||
let keep = result.tessellate_with(1e-3, true).unwrap().contains(probe);
|
||||
let keep = result.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().contains(probe);
|
||||
eprintln!("public Union result web={keep}");
|
||||
keep
|
||||
}
|
||||
@@ -111,7 +111,7 @@ fn unproved_curved_boundary_between_clear_endpoints_refuses_recut() {
|
||||
};
|
||||
let body = extrude(&Plane::XY, &[region], 16.05, 16.07).unwrap().remove(0);
|
||||
assert!(a.len() > 5. && b.len() > 5., "endpoint samples miss the interior excursion");
|
||||
assert!(body.tessellate_with(1e-4, true).unwrap().contains(Vec3::new(4.5, 0., 16.06)));
|
||||
assert!(body.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().contains(Vec3::new(4.5, 0., 16.06)));
|
||||
let before = step_export(&[&body], "curved source").unwrap();
|
||||
assert!(crate::bore_guard::boundaries_clear(&body, &[], Vec3::ZERO, UnitVec3::Z, 5., 14., 24., 1e-6).is_none());
|
||||
assert_eq!(step_export(&[&body], "curved source").unwrap(), before);
|
||||
@@ -129,7 +129,7 @@ fn coplanar_faces_apart_from_each_other_do_not_coincide() {
|
||||
// A post over the first one's top does meet it there.
|
||||
assert!(!crate::coplanar::coincident_planes(&with_one, &cylinder(Vec3::new(12., 30., 7.), Vec3::Z, 3., 6.).unwrap()).is_empty());
|
||||
let joined = boolean(&with_one, &post(60.), BoolOp::Union).unwrap().unwrap();
|
||||
let v = joined.tessellate_with(1e-3, true).unwrap().measure().volume;
|
||||
let v = joined.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().measure().volume;
|
||||
let expected = 80. * 60. * 2. + 2. * std::f64::consts::PI * 9. * 5.;
|
||||
assert!((v - expected).abs() < 0.5, "volume {v}, expected {expected}");
|
||||
}
|
||||
|
||||
@@ -560,9 +560,9 @@ mod tests {
|
||||
let near = |a: f64| (a - want).abs() < 0.25;
|
||||
let faces = body.faces(0.01).unwrap();
|
||||
assert!(faces.len() == 1 && near(faces[0].area), "faces: {faces:?}");
|
||||
let tess = body.tessellate(0.01).unwrap().measure().area;
|
||||
let tess = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().area;
|
||||
assert!(near(tess), "measuring mesh: {tess}");
|
||||
let display = body.display_mesh(0.01).unwrap().measure().area;
|
||||
let display = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().area;
|
||||
assert!(near(display), "display mesh: {display}");
|
||||
}
|
||||
|
||||
@@ -586,9 +586,9 @@ mod tests {
|
||||
let near = |a: f64| (a - want).abs() < 0.2;
|
||||
let faces = body.faces(0.01).unwrap();
|
||||
assert!(near(faces[0].area), "faces: {faces:?}");
|
||||
let tess = body.tessellate(0.01).unwrap().measure().area;
|
||||
let tess = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().area;
|
||||
assert!(near(tess), "measuring mesh: {tess}");
|
||||
let display = body.display_mesh(0.01).unwrap().measure().area;
|
||||
let display = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().area;
|
||||
assert!(near(display), "display mesh: {display}");
|
||||
// Two collapsed sides are no patch.
|
||||
let p = |l: &str, q: Vec3| BoundaryCurve { label: l.into(), source: CurveSource::Point(q), continuity: Continuity::G0 };
|
||||
|
||||
@@ -42,7 +42,7 @@ fn operands() -> (Body, Body) {
|
||||
|
||||
fn reach(a: &Body, b: &Body) -> Reach {
|
||||
let size = a.size().max(b.size());
|
||||
let meshes = (a.tessellate_with(size * 5e-4, true).unwrap(), b.tessellate_with(size * 5e-4, true).unwrap());
|
||||
let meshes = (a.mesh(&crate::MeshTolerance::chord(size * 5e-4)).unwrap(), b.mesh(&crate::MeshTolerance::chord(size * 5e-4)).unwrap());
|
||||
Reach::new(a, b, (Some(&meshes.0), Some(&meshes.1)), size).unwrap()
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ fn large_faces_still_mesh() {
|
||||
let plate = crate::box_solid(Vec3::new(-2500.0, -2500.0, 0.0), Vec3::new(2500.0, 2500.0, 3.0)).unwrap();
|
||||
let rod = crate::cylinder(Vec3::ZERO, Vec3::Z, 0.5, 3000.0).unwrap();
|
||||
for body in [plate, rod] {
|
||||
assert!(!body.tessellate_with(0.05, true).unwrap().triangles.is_empty());
|
||||
assert!(!body.mesh(&crate::MeshTolerance::chord(0.05)).unwrap().triangles.is_empty());
|
||||
assert_eq!(body.unmeshed_faces(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ const MAX_PAIRS: usize = 2_000_000;
|
||||
impl<'a> Clearance<'a> {
|
||||
/// `chord`: the chord error of the mesh the body is asked on.
|
||||
pub fn new(body: &'a Body, chord: f64) -> Option<Clearance<'a>> {
|
||||
let mesh = body.tessellate_with(chord, true).ok()?;
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(chord)).ok()?;
|
||||
(!mesh.triangles.is_empty()).then(|| Clearance { body, mesh, sag: 2.0 * chord, tol: (body.size() * 1e-7).max(1e-9), exact: false })
|
||||
}
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ fn flush(x: &Body, y: &Body, same: f64, depth: f64) -> Vec<usize> {
|
||||
return Vec::new();
|
||||
}
|
||||
let tol = (x.size().max(y.size()) * 2e-3).max(1e-4);
|
||||
let (Ok(mx), Ok(my)) = (x.tessellate(tol), y.tessellate(tol)) else { return Vec::new() };
|
||||
let (Ok(mx), Ok(my)) = (x.mesh(&crate::MeshTolerance::chord(tol)), y.mesh(&crate::MeshTolerance::chord(tol))) else { return Vec::new() };
|
||||
// At most this many facets of a face are sampled.
|
||||
const SAMPLES: usize = 64;
|
||||
level
|
||||
@@ -379,7 +379,7 @@ fn cylinders(b: &Body) -> Vec<Wall> {
|
||||
/// its material is on and where they run along the axis. `None` when the body can't be meshed.
|
||||
fn walls(b: &Body, cylinders: Vec<Wall>, same: f64) -> Option<Vec<Wall>> {
|
||||
let mut out: Vec<Wall> = Vec::new();
|
||||
let mesh = b.tessellate_with((b.size() * 1e-3).max(1e-4), true).ok()?;
|
||||
let mesh = b.mesh(&crate::MeshTolerance::chord((b.size() * 1e-3).max(1e-4))).ok()?;
|
||||
for Wall { faces, origin, axis, radius, .. } in cylinders {
|
||||
let Some(&face) = faces.first() else { continue };
|
||||
// Which way the face looks: out of the axis for a pin, toward it for a hole.
|
||||
|
||||
@@ -990,7 +990,7 @@ pub(crate) fn faces_touch(a: &Body, b: &Body) -> bool {
|
||||
if faces.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let (Ok(ma), Ok(mb)) = (a.tessellate_with(size * 1e-3, true), b.tessellate_with(size * 1e-3, true)) else { return false };
|
||||
let (Ok(ma), Ok(mb)) = (a.mesh(&crate::MeshTolerance::chord(size * 1e-3)), b.mesh(&crate::MeshTolerance::chord(size * 1e-3))) else { return false };
|
||||
let inside = ma.inside_index();
|
||||
let depth = (size * 1e-4).max(1e-6);
|
||||
mb.triangles.iter().zip(&mb.tri_face).take(100_000).any(|(t, fi)| {
|
||||
@@ -1044,7 +1044,7 @@ const MAX_SKIN_FACES: usize = 2000;
|
||||
/// the chord error.
|
||||
fn in_plane(tool: &Body, m: Vec3, d: f64, skin: f64) -> Vec<([Vec3; 3], f64)> {
|
||||
let chord = (tool.size() * 2e-4).max(skin);
|
||||
let Ok(mesh) = tool.tessellate_with(chord, true) else { return Vec::new() };
|
||||
let Ok(mesh) = tool.mesh(&crate::MeshTolerance::chord(chord)) else { return Vec::new() };
|
||||
let faces: Vec<&mt::Face> = tool.solid.face_iter().collect();
|
||||
let margins: HashMap<usize, f64> = planes(tool)
|
||||
.into_iter()
|
||||
@@ -1063,7 +1063,7 @@ fn backed(a: &Body, b: &Body, m: Vec3, d: f64, depth: f64) -> bool {
|
||||
let size = a.size().max(b.size());
|
||||
let tol = (size * 1e-7).max(1e-9) * 10.0;
|
||||
let faces: Vec<usize> = planes(b).into_iter().filter(|(_, p)| p.n.dot(m) > 1.0 - 1e-9 && (p.d - d).abs() < tol).map(|(i, _)| i).collect();
|
||||
let (Ok(ma), Ok(mb)) = (a.tessellate_with(size * 1e-3, true), b.tessellate_with(size * 1e-3, true)) else { return false };
|
||||
let (Ok(ma), Ok(mb)) = (a.mesh(&crate::MeshTolerance::chord(size * 1e-3)), b.mesh(&crate::MeshTolerance::chord(size * 1e-3))) else { return false };
|
||||
let inside = ma.inside_index();
|
||||
let mut points = Vec::new();
|
||||
for (t, fi) in mb.triangles.iter().zip(&mb.tri_face) {
|
||||
@@ -1589,7 +1589,7 @@ pub(crate) fn bore_union(a: &Body, b: &Body, accept: &dyn Fn(&Body) -> bool) ->
|
||||
let size = a.size().max(b.size());
|
||||
let tol = (size * 1e-7).max(1e-9) * 10.0;
|
||||
let mtol = size * 2e-3;
|
||||
let (ma, mb) = (a.tessellate_with(mtol, true).ok()?, b.tessellate_with(mtol, true).ok()?);
|
||||
let (ma, mb) = (a.mesh(&crate::MeshTolerance::chord(mtol)).ok()?, b.mesh(&crate::MeshTolerance::chord(mtol)).ok()?);
|
||||
let (ia, ib) = (ma.inside_index(), mb.inside_index());
|
||||
let empty = |p: Vec3| !ia.contains(p) && !ib.contains(p);
|
||||
let (ca, cb) = (cylinders(a), cylinders(b));
|
||||
|
||||
@@ -122,7 +122,7 @@ fn a_block_flush_with_the_flat_face_of_a_hemisphere_joins_exactly() {
|
||||
assert_eq!(joined.lumps().unwrap().len(), 1);
|
||||
assert!(joined.validity().is_empty(), "{:?}", joined.validity());
|
||||
// The flat faces are one plane again: nothing of the join stays below z = 0.
|
||||
let mesh = joined.tessellate(0.05).unwrap();
|
||||
let mesh = joined.mesh(&crate::MeshTolerance::chord(0.05)).unwrap();
|
||||
let lowest = mesh.positions.iter().map(|p| p.z).fold(f64::MAX, f64::min);
|
||||
assert!(lowest > -1e-6, "the join reaches down to z = {lowest}");
|
||||
}
|
||||
@@ -141,7 +141,7 @@ fn what_growing_a_face_adds_is_cut_off_again_exactly() {
|
||||
assert!((v - measured).abs() < measured * 1e-6, "{v} against {measured}");
|
||||
assert_eq!(back.face_count(), dome.face_count());
|
||||
assert!(back.validity().is_empty(), "{:?}", back.validity());
|
||||
let lowest = back.tessellate(0.05).unwrap().positions.iter().map(|p| p.z).fold(f64::MAX, f64::min);
|
||||
let lowest = back.mesh(&crate::MeshTolerance::chord(0.05)).unwrap().positions.iter().map(|p| p.z).fold(f64::MAX, f64::min);
|
||||
assert!(lowest > -1e-6, "the body reaches down to z = {lowest}");
|
||||
}
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ fn a_law_running_to_zero_along_a_curved_edge_is_refused() {
|
||||
// where nothing closes it.
|
||||
let made = fillet_variable(&body, mid, start, 3.0, 2.4).unwrap();
|
||||
assert!(made.validity().is_empty(), "{:?}", made.validity());
|
||||
let mesh = made.tessellate_with(5e-4, true).unwrap();
|
||||
let mesh = made.mesh(&crate::MeshTolerance::chord(5e-4)).unwrap();
|
||||
let about = |o: Vec3| {
|
||||
mesh.triangles
|
||||
.iter()
|
||||
|
||||
@@ -42,7 +42,7 @@ fn issue_1303_chamfer_open_rim_after_end_chamfer() {
|
||||
// The straight strips end at min(31-u, 30+v), 0 <= v <= u. Their integral
|
||||
// over 0 <= u <= 1 is 30 + 1/6; this includes both split end sections.
|
||||
let expected = 30.0 + 1.0 / 6.0 + std::f64::consts::PI * (11.75 / 2.0 + 1.0 / 6.0);
|
||||
let volume = |b: &Body| b.tessellate_with(0.0001, true).unwrap().measure().volume;
|
||||
let volume = |b: &Body| b.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
|
||||
let removed = volume(&body) - volume(&result);
|
||||
assert!((removed - expected).abs() < 0.02, "removed {removed}, expected {expected}");
|
||||
for p in [Vec3::new(30.5, 12.25, 12.5), Vec3::new(30.5, -12.25, 12.5)] {
|
||||
@@ -176,7 +176,7 @@ fn rolling_ball_matches_analytic_tee_and_contacts() {
|
||||
|
||||
// The reverse direction uses points on the actual trimmed face mesh, so an
|
||||
// untrimmed neighbouring surface cannot conceal an overshooting blend patch.
|
||||
let mesh = blend.tessellate_with(0.002, true).unwrap();
|
||||
let mesh = blend.mesh(&crate::MeshTolerance::chord(0.002)).unwrap();
|
||||
let mut reverse_distance: f64 = 0.0;
|
||||
let mut probes = 0;
|
||||
for point in mesh.positions.iter().step_by(19) {
|
||||
@@ -315,7 +315,7 @@ fn rolling_ball_along_a_straight_edge_between_cylinders_trims_curved_end_seams()
|
||||
assert_eq!((after.faces, after.edges, after.vertices), (before.faces + 1, before.edges + 3, before.vertices + 2), "{before:?} -> {after:?}");
|
||||
// The round fills the gap over the full height. The caps gain it twice; the walls lose
|
||||
// their arcs to the contacts and gain the round.
|
||||
let (m0, m1) = (body.tessellate_with(0.0005, true).unwrap().measure(), blend.tessellate_with(0.0005, true).unwrap().measure());
|
||||
let (m0, m1) = (body.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap().measure(), blend.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap().measure());
|
||||
let volume = h * g.area;
|
||||
assert!((m1.volume - m0.volume - volume).abs() < volume * 3e-3, "added volume {} vs {volume}", m1.volume - m0.volume);
|
||||
let [a1, a2, af] = g.angles;
|
||||
@@ -517,7 +517,7 @@ fn rolling_ball_along_an_arc_ends_in_side_walls_at_an_angle() {
|
||||
let surfaces: Vec<_> =
|
||||
blend.solid.face_iter().map(|f| f.oriented_surface()).filter(|s| matches!(s, mt::Surface::NurbsSurface(_))).collect();
|
||||
assert!(!surfaces.is_empty(), "{what}: no blend face");
|
||||
let mesh = blend.tessellate_with(0.0005, true).unwrap();
|
||||
let mesh = blend.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap();
|
||||
let (lo, hi) = mesh.positions.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| (lo.min(p.x), hi.max(p.x)));
|
||||
assert!(lo > -1e-3 && hi < 40.0 + 1e-3, "{what}: the blend runs past the side walls: x {lo}..{hi}");
|
||||
if round {
|
||||
@@ -645,7 +645,7 @@ fn rolling_ball_along_a_straight_edge_ends_in_a_curved_face() {
|
||||
.sum::<f64>()
|
||||
* h
|
||||
/ 3.0;
|
||||
let (m0, m1) = (body.tessellate_with(0.0005, true).unwrap(), blend.tessellate_with(0.0005, true).unwrap());
|
||||
let (m0, m1) = (body.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap(), blend.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap());
|
||||
let got = m0.measure().volume - m1.measure().volume;
|
||||
assert!((got - removed).abs() < removed * 3e-3, "round {round}: removed {got} vs {removed}");
|
||||
let volume_about = |o: Vec3| {
|
||||
@@ -676,7 +676,7 @@ fn chamfer_a_chain_of_a_line_and_an_arc_into_a_slanted_end() {
|
||||
Seg2::Line { a: Vec2::new(0.0, 40.0), b: Vec2::new(0.0, 0.0) },
|
||||
];
|
||||
let body = crate::extrude(&Plane::XY, &[Region2 { outer: Loop2 { segs }, holes: vec![] }], 0.0, 20.0).unwrap().pop().unwrap();
|
||||
let volume = |b: &Body| b.tessellate_with(0.0005, true).unwrap().measure().volume;
|
||||
let volume = |b: &Body| b.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap().measure().volume;
|
||||
let made = crate::chamfer(&body, &[Vec3::new(10.0, 0.0, 20.0)], 2.0).unwrap();
|
||||
let n = 2000;
|
||||
let h = 2.0 / n as f64;
|
||||
|
||||
@@ -291,7 +291,7 @@ fn corner_blends_hold_at_any_scale() {
|
||||
// What a blend takes off the part scaled by `k`, over k³, through a display plan of matching
|
||||
// accuracy.
|
||||
let removed = |k: f64, steps: &[Vec<Vec3>]| {
|
||||
let volume = |b: &Body| b.display_plan(2e-4 * k).unwrap().evaluate().measure().volume;
|
||||
let volume = |b: &Body| b.mesh_plan(&crate::MeshTolerance::chord(2e-4 * k)).unwrap().evaluate().measure().volume;
|
||||
let body = scaled_arc_part(k);
|
||||
let mut b = body.clone();
|
||||
for pts in steps {
|
||||
|
||||
@@ -342,12 +342,12 @@ mod tests {
|
||||
use crate::{BoolOp, boolean, box_solid, chamfer, cylinder, fillet};
|
||||
|
||||
fn volume(b: &crate::Body) -> f64 {
|
||||
b.tessellate(1e-3).unwrap().measure().volume
|
||||
b.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().measure().volume
|
||||
}
|
||||
|
||||
/// The faces of `b` with every triangle corner passing `keep`.
|
||||
fn faces_where(b: &crate::Body, keep: impl Fn(Vec3) -> bool) -> Vec<usize> {
|
||||
let m = b.tessellate(1e-3).unwrap();
|
||||
let m = b.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap();
|
||||
let n = m.tri_face.iter().max().map(|x| *x as usize + 1).unwrap_or(0);
|
||||
(0..n)
|
||||
.filter(|f| {
|
||||
|
||||
@@ -156,8 +156,8 @@ fn a_small_bend_on_a_large_sheet_is_meshed_round() {
|
||||
let tol = display_tol(&b);
|
||||
// The tolerance is larger than either side's sagitta: 1 - cos 45° of the radius.
|
||||
assert!(tol > 3.0 * (1.0 - (PI / 4.0).cos()));
|
||||
let coarse = b.display_mesh(tol).unwrap();
|
||||
let round = b.display_mesh_angular(tol, DISPLAY_ANGLE).unwrap();
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
let round = b.mesh(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
// A quarter turn in facets of at most DISPLAY_ANGLE.
|
||||
let facets = (FRAC_PI_2 / DISPLAY_ANGLE).ceil() as usize;
|
||||
assert_eq!(facets, 5);
|
||||
@@ -197,8 +197,8 @@ fn a_small_bend_on_a_large_sheet_is_meshed_round() {
|
||||
fn the_round_display_mesh_stays_closed_and_keeps_its_edges_and_bounds() {
|
||||
let b = bent_sheet(3000.0);
|
||||
let tol = display_tol(&b);
|
||||
let coarse = b.display_mesh(tol).unwrap();
|
||||
let round = b.display_mesh_angular(tol, DISPLAY_ANGLE).unwrap();
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
let round = b.mesh(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
// Neighbouring faces share the points of the edge between them.
|
||||
assert_eq!(open_sides(&round, 1e-6), 0);
|
||||
// The same edges in the same order, between the same faces, with the same ends and flags.
|
||||
@@ -223,8 +223,8 @@ fn a_small_hole_on_a_large_plate_has_a_round_rim() {
|
||||
let b = plate_with_holes(&[centre], 2.0);
|
||||
let tol = display_tol(&b);
|
||||
assert!(tol > 2.0, "the hole's radius is below the chord tolerance");
|
||||
let coarse = b.display_mesh(tol).unwrap();
|
||||
let round = b.display_mesh_angular(tol, DISPLAY_ANGLE).unwrap();
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
let round = b.mesh(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
let segments = (TAU / DISPLAY_ANGLE).ceil() as usize;
|
||||
assert_eq!(segments, 18);
|
||||
for z in [0.0, 2.0] {
|
||||
@@ -249,15 +249,15 @@ fn a_small_hole_on_a_large_plate_has_a_round_rim() {
|
||||
fn an_arc_the_chord_tolerance_already_divides_finely_is_left_alone() {
|
||||
// A 40 mm cylinder at a tolerance of 0.01 mm: 64 facets around, far inside the angle.
|
||||
let b = crate::cylinder(Vec3::ZERO, Vec3::Z, 20.0, 30.0).unwrap();
|
||||
let coarse = b.display_mesh(0.01).unwrap();
|
||||
let round = b.display_mesh_angular(0.01, DISPLAY_ANGLE).unwrap();
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
let round = b.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
assert_eq!(round.positions, coarse.positions);
|
||||
assert_eq!(round.triangles, coarse.triangles);
|
||||
assert_eq!(round.edges, coarse.edges);
|
||||
// And a body with nothing curved at any tolerance.
|
||||
let b = crate::box_solid(Vec3::ZERO, Vec3::new(3000.0, 1000.0, 2.0)).unwrap();
|
||||
let tol = display_tol(&b);
|
||||
assert_eq!(b.display_mesh_angular(tol, DISPLAY_ANGLE).unwrap().positions, b.display_mesh(tol).unwrap().positions);
|
||||
assert_eq!(b.mesh(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap().positions, b.mesh(&crate::MeshTolerance::chord(tol)).unwrap().positions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -268,14 +268,14 @@ fn a_radius_far_below_the_tolerance_keeps_its_chord_facets() {
|
||||
let centre = Vec2::new(30.0, 20.0);
|
||||
let b = plate_with_holes(&[centre], 0.002);
|
||||
let tol = display_tol(&b);
|
||||
let coarse = b.display_mesh(tol).unwrap();
|
||||
let round = b.display_mesh_angular(tol, DISPLAY_ANGLE).unwrap();
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
let round = b.mesh(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
assert_eq!(round.triangles.len(), coarse.triangles.len());
|
||||
assert_eq!(round.edges, coarse.edges);
|
||||
// Just above the floor the rim is divided, but coarsely: the angle stops where the sag
|
||||
// falls below the floor, not where the facets reach the angle.
|
||||
let b = plate_with_holes(&[centre], 0.01);
|
||||
let round = b.display_mesh_angular(display_tol(&b), DISPLAY_ANGLE).unwrap();
|
||||
let round = b.mesh(&crate::MeshTolerance { chord: display_tol(&b), angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
let (n, _) = rim(&round, centre, 0.01, 2.0);
|
||||
assert!((3..18).contains(&n), "{n} segments");
|
||||
}
|
||||
@@ -288,8 +288,8 @@ fn a_face_crowded_with_holes_gets_a_coarser_angle_and_bounded_work() {
|
||||
let centres: Vec<Vec2> = (0..50).flat_map(|i| (0..20).map(move |j| Vec2::new(30.0 + 60.0 * i as f64, 25.0 + 50.0 * j as f64))).collect();
|
||||
let b = plate_with_holes(¢res, 2.0);
|
||||
let tol = display_tol(&b);
|
||||
let coarse = b.display_mesh(tol).unwrap();
|
||||
let round = b.display_mesh_angular(tol, DISPLAY_ANGLE).unwrap();
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
let round = b.mesh(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
let boundary = |m: &Mesh, z: f64| m.edges.iter().filter(|e| e.iter().all(|p| (p.z - z).abs() < 1e-6)).map(|e| e.len() - 1).sum::<usize>();
|
||||
let budget = truck_meshalgo::tessellation::Deflection::MAX_ANGLE_BOUNDARY;
|
||||
for z in [0.0, 2.0] {
|
||||
@@ -313,7 +313,7 @@ fn a_face_crowded_with_holes_gets_a_coarser_angle_and_bounded_work() {
|
||||
assert_eq!(open_sides(&round, 1e-6), 0);
|
||||
// A plate with few holes keeps the full angle.
|
||||
let few = plate_with_holes(¢res[..40], 2.0);
|
||||
let round = few.display_mesh_angular(display_tol(&few), DISPLAY_ANGLE).unwrap();
|
||||
let round = few.mesh(&crate::MeshTolerance { chord: display_tol(&few), angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
assert!(rim(&round, centres[0], 2.0, 2.0).0 >= 18);
|
||||
}
|
||||
|
||||
@@ -340,8 +340,8 @@ fn round_faces_of_every_kind_are_divided_by_the_angle() {
|
||||
.fold(0.0, f64::max)
|
||||
};
|
||||
let check = |name: &str, b: &Body| {
|
||||
let coarse = b.display_mesh(3.0).unwrap();
|
||||
let round = b.display_mesh_angular(3.0, DISPLAY_ANGLE).unwrap();
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(3.0)).unwrap();
|
||||
let round = b.mesh(&crate::MeshTolerance { chord: 3.0, angle: Some(DISPLAY_ANGLE) }).unwrap();
|
||||
// The chord tolerance alone leaves facets coarser than the angle (the size rules' longest
|
||||
// side and aspect limit already keep them near 40°), so the angle has work to do.
|
||||
assert!(turn(b, &coarse) > 1.5 * DISPLAY_ANGLE, "{name}: the chord tolerance leaves facets of {} degrees", turn(b, &coarse).to_degrees());
|
||||
@@ -377,7 +377,7 @@ fn a_cylinder_wall_split_along_its_seam_meshes_round() {
|
||||
.collect();
|
||||
assert_eq!(outer.iter().filter(|o| **o).count(), 2);
|
||||
for tol in [display_tol(&cup), 0.1, 0.01] {
|
||||
let m = cup.tessellate(tol).unwrap();
|
||||
let m = cup.mesh(&crate::MeshTolerance::chord(tol)).unwrap();
|
||||
assert_eq!(open_sides(&m, 1e-6), 0);
|
||||
let v = mesh_volume(&m);
|
||||
assert!((v - exact).abs() < exact * 0.01, "tolerance {tol}: mesh volume {v:.1}, exact {exact:.1}");
|
||||
@@ -403,7 +403,7 @@ fn outline_bounds_hold_the_display_mesh() {
|
||||
let rod = crate::cylinder(Vec3::new(5., 5., 0.), Vec3::Z, 4., 25.).unwrap();
|
||||
for body in [cube, rounded, rod] {
|
||||
let outline = body.outline_bounds();
|
||||
let mesh = body.reference_display_mesh_angular(body.size() * 1e-3, crate::DISPLAY_ANGLE).unwrap().bounds();
|
||||
let mesh = body.mesh(&crate::MeshTolerance { chord: body.size() * 1e-3, angle: Some(crate::DISPLAY_ANGLE) }).unwrap().bounds();
|
||||
let slack = body.size() * 1e-6;
|
||||
for (lo, hi, m_lo, m_hi) in [
|
||||
(outline.min.x, outline.max.x, mesh.min.x, mesh.max.x),
|
||||
|
||||
+13
-13
@@ -615,9 +615,9 @@ mod tests {
|
||||
Region2 { outer: Loop2::polygon(&[Vec2::new(-5., 5.), Vec2::new(-1., 5.), Vec2::new(-1., 10.), Vec2::new(-5., 10.)]), holes: vec![] };
|
||||
let (_, other_anchor) = infer_cylinder_emboss(&near, &shifted, std::slice::from_ref(&shifted_profile), 1.).unwrap().unwrap();
|
||||
assert!(other_anchor.dist(anchor) < 1e-10);
|
||||
let before = near.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let before = near.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let result = emboss_cylinder_at(&near, other_anchor, &shifted, &[shifted_profile], 1.).unwrap();
|
||||
assert!((result.tessellate_with(1e-4, true).unwrap().measure().volume - before - 21.).abs() < 0.02);
|
||||
assert!((result.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume - before - 21.).abs() < 0.02);
|
||||
assert!(infer_cylinder_emboss(&near, &plane(), ®ions, -10.).is_err());
|
||||
let inward = Plane::new(Vec3::Y * 10., Vec3::X, Vec3::Z).unwrap();
|
||||
assert!(infer_cylinder_emboss(&near, &inward, ®ions, 1.).unwrap_err().to_string().contains("outwards"));
|
||||
@@ -629,16 +629,16 @@ mod tests {
|
||||
ring.holes.push(Loop2::polygon(&[Vec2::new(-1., 6.), Vec2::new(1., 6.), Vec2::new(1., 9.), Vec2::new(-1., 9.)]));
|
||||
let island =
|
||||
Region2 { outer: Loop2::polygon(&[Vec2::new(-0.5, 6.5), Vec2::new(0.5, 6.5), Vec2::new(0.5, 8.5), Vec2::new(-0.5, 8.5)]), holes: vec![] };
|
||||
let before = body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let before = body.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
for depth in [1., -1.] {
|
||||
for regions in [vec![ring.clone(), island.clone()], vec![island.clone(), ring.clone()]] {
|
||||
let result = emboss_cylinder(&body, &plane(), ®ions, depth).unwrap();
|
||||
assert_eq!(result.shell_count(), 1);
|
||||
assert!(result.validity().is_empty());
|
||||
let after = result.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let after = result.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let expected = 16. * (depth + depth * depth / 20.);
|
||||
assert!((after - before - expected).abs() < 0.02, "{} vs {expected}", after - before);
|
||||
let mesh = result.tessellate_with(0.001, true).unwrap();
|
||||
let mesh = result.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
for (s, raised) in [(0., true), (0.75, false), (1.5, true)] {
|
||||
let radial = -Vec3::X * (s / 10.0_f64).sin() + Vec3::Y * (s / 10.0_f64).cos();
|
||||
let radius = if depth > 0. { 10.5 } else { 9.5 };
|
||||
@@ -655,11 +655,11 @@ mod tests {
|
||||
let result = emboss_cylinder(&body, &plane(), &[profile()], depth).unwrap();
|
||||
assert_eq!(result.shell_count(), 1);
|
||||
assert!(result.validity().is_empty());
|
||||
let before = body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let after = result.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let before = body.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let after = result.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let expected = if depth > 0. { 21. } else { -19. };
|
||||
assert!((after - before - expected).abs() < 0.02, "{} vs {expected}", after - before);
|
||||
let mesh = result.tessellate_with(0.001, true).unwrap();
|
||||
let mesh = result.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
for s in [-1.5, 0., 1.5] {
|
||||
let radial = -Vec3::X * (s / 10.0_f64).sin() + Vec3::Y * (s / 10.0_f64).cos();
|
||||
assert_eq!(mesh.contains(radial * (if depth > 0. { 10.5 } else { 9.5 }) + Vec3::Z * 7.5), depth > 0.);
|
||||
@@ -692,12 +692,12 @@ mod tests {
|
||||
for depth in [1., -1.] {
|
||||
let result = emboss_cylinder_at(&body, origin + radial * 10.0 + axis * 7.5, &plane, std::slice::from_ref(&profile), depth).unwrap();
|
||||
assert_eq!(result.shell_count(), 1);
|
||||
let before = body.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let after = result.tessellate_with(1e-4, true).unwrap().measure().volume;
|
||||
let before = body.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let after = result.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
let want = profile.area().abs() * (depth + depth * depth / 20.0);
|
||||
assert!((after - before - want).abs() < 0.02, "{} vs {want}", after - before);
|
||||
let point = |s: f64, z: f64, r: f64| origin + axis * z + (radial * (s / 10.0).cos() + tangent * (s / 10.0).sin()) * r;
|
||||
let mesh = result.tessellate_with(0.001, true).unwrap();
|
||||
let mesh = result.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
for (s, z) in [(-1.5, 7.5), (1.5, 7.5), (0.5, 6.5)] {
|
||||
let material = profile.contains(Vec2::new(s, z));
|
||||
assert_eq!(mesh.contains(point(s, z, if depth > 0. { 10.5 } else { 9.5 })), if depth > 0. { material } else { !material });
|
||||
@@ -707,7 +707,7 @@ mod tests {
|
||||
assert_eq!(imported.bodies.len(), 1);
|
||||
let round = imported.bodies.first().unwrap();
|
||||
assert!(round.body.validity().is_empty());
|
||||
let roundmesh = round.body.tessellate_with(0.001, true).unwrap();
|
||||
let roundmesh = round.body.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
assert_eq!(roundmesh.contains(point(-1.5, 7.5, if depth > 0. { 10.5 } else { 9.5 })), depth > 0.);
|
||||
let cut = crate::boolean(
|
||||
&round.body,
|
||||
@@ -716,7 +716,7 @@ mod tests {
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(!cut.tessellate(0.01).unwrap().contains(point(-1.5, 7.5, 9.0)));
|
||||
assert!(!cut.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().contains(point(-1.5, 7.5, 9.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{FaceSource, extrude, transform_matrix};
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
|
||||
fn assert_exact_planes(body: &crate::Body) {
|
||||
let mesh = body.tessellate(0.001).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
for (index, _) in body.face_sources().unwrap().iter().enumerate() {
|
||||
let Some(plane) = body.face_plane(index) else { continue };
|
||||
let mut count = 0;
|
||||
@@ -81,7 +81,7 @@ fn cap_roles_follow_exact_offset_planes_and_canonical_profile_memberships() {
|
||||
assert_eq!(caps.len(), 2);
|
||||
assert_exact_planes(&body);
|
||||
let expected = (400. - 5. * std::f64::consts::PI) * (end - start).abs();
|
||||
assert!((body.tessellate(1e-4).unwrap().measure().volume - expected).abs() < 0.03);
|
||||
assert!((body.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume - expected).abs() < 0.03);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,7 @@ fn material_changes_preserve_only_unchanged_planar_support_certificates() {
|
||||
let base = crate::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap().with_box_certificate(42).unwrap();
|
||||
let tool = crate::box_solid(Vec3::new(5., -1., -1.), Vec3::new(11., 11., 11.)).unwrap();
|
||||
let cut = crate::boolean(&base, &tool, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
assert!((cut.tessellate(1e-4).unwrap().measure().volume - 500.).abs() < 1e-8);
|
||||
assert!((cut.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume - 500.).abs() < 1e-8);
|
||||
// Unchanged uniquely certified supports retain their role; a newly cut
|
||||
// support must never inherit the removed opposite wall's source identity.
|
||||
let mut retained = 0;
|
||||
@@ -236,7 +236,7 @@ fn constructed_annulus_sides_follow_exact_source_edges() {
|
||||
let body = &bodies[0];
|
||||
let sources = body.face_sources().unwrap();
|
||||
assert_eq!(sources.len(), body.face_count());
|
||||
let mesh = body.tessellate(0.001).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
let mut inner = 0;
|
||||
let mut outer = 0;
|
||||
for (index, source) in sources.iter().enumerate() {
|
||||
@@ -301,7 +301,7 @@ fn constructed_large_arc_split_and_reversal_preserve_one_source_segment() {
|
||||
assert_eq!(arcs.len(), 2, "large input arc explicitly creates two actual boundary edges");
|
||||
assert!(arcs.iter().all(|(radius, sweep)| (**radius - 10.).abs() < 1e-12 && (sweep.abs() - std::f64::consts::PI * 5. / 3.).abs() < 1e-12));
|
||||
assert_eq!(sources.iter().filter(|source| matches!(source, Some(FaceSource::ExtrudeSide { segment: Seg2::Line { .. }, .. }))).count(), 1);
|
||||
let mesh = body.tessellate(0.001).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
if matches!(sources[*face as usize], Some(FaceSource::ExtrudeSide { segment: Seg2::Arc { .. }, .. })) {
|
||||
assert!(mesh.tri(triangle).unwrap().iter().all(|point| (point.x.hypot(point.y) - 10.).abs() < 1e-7));
|
||||
|
||||
@@ -438,7 +438,7 @@ mod tests {
|
||||
/// The fill's mesh: every normal finite, and no triangle folded over (all face the same way
|
||||
/// as the mesh's total area vector, by a clear margin).
|
||||
fn mesh_is_clean(fill: &Body) -> f64 {
|
||||
let m = fill.tessellate(0.01).unwrap();
|
||||
let m = fill.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
assert!(!m.triangles.is_empty());
|
||||
assert!(m.normals.iter().all(|n| n.is_finite() && n.len() > 0.5), "a mesh normal is not finite");
|
||||
let tri = |t: &[u32; 3]| m.tri(t).unwrap();
|
||||
|
||||
@@ -171,7 +171,7 @@ fn check_face(name: &str, open: &Body, fill: &Body) -> f64 {
|
||||
off = off.max(from_p3(s.subs(uv.0, uv.1)).dist(p));
|
||||
}
|
||||
assert!(off <= tol * 1.01, "{name}: an edge is {off} off the face (tolerance {tol})");
|
||||
let m = fill.tessellate(0.01).unwrap();
|
||||
let m = fill.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
assert!(!m.triangles.is_empty(), "{name}: the face meshes");
|
||||
for t in &m.triangles {
|
||||
let [a, b, c] = m.tri(t).unwrap();
|
||||
|
||||
@@ -167,7 +167,7 @@ fn smooth_helix(plane: &Plane, profile: &NumberedLoop, o: Vec3, k: Vec3, pitch:
|
||||
let section_tol = (lp.signed_area().abs().sqrt() * 0.05).max(1e-3);
|
||||
for ws in [walls.clone(), walls.iter().map(|(f, prov)| (f.inverse(), prov.clone())).collect()] {
|
||||
if let Ok(b) = cap_and_close(ws, std::slice::from_ref(&w0), std::slice::from_ref(&w1), false)
|
||||
&& b.tessellate(section_tol).is_ok_and(|m| (m.measure().volume - expected).abs() <= 0.15 * expected.abs())
|
||||
&& b.mesh(&crate::MeshTolerance::chord(section_tol)).is_ok_and(|m| (m.measure().volume - expected).abs() <= 0.15 * expected.abs())
|
||||
{
|
||||
return Ok(b);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ fn a_transform_keeps_every_face_and_edge_in_place() {
|
||||
assert_eq!(ea, em);
|
||||
assert!(h.edges.iter().all(|e| e.faces.len() == 2 && e.origins.len() == 1));
|
||||
// The history's edges are the tessellation's edges, bounding the same faces.
|
||||
let mesh = moved.tessellate(1e-3).unwrap();
|
||||
let mesh = moved.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap();
|
||||
let mesh_faces: Vec<Vec<usize>> = mesh.edge_faces.iter().map(|fs| fs.iter().map(|f| *f as usize).collect()).collect();
|
||||
assert_eq!(mesh_faces, h.edges.iter().map(|e| e.faces.clone()).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ fn an_arc_revolved_about_its_centre_is_a_cap_not_a_ball() {
|
||||
];
|
||||
let imp = iges_import(&iges_file_placed(&ents)).unwrap();
|
||||
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
|
||||
let mesh = imp.bodies[0].body.tessellate(0.01).unwrap();
|
||||
let mesh = imp.bodies[0].body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
let (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]);
|
||||
for p in &mesh.positions {
|
||||
for (k, x) in [p.x, p.y, p.z].into_iter().enumerate() {
|
||||
|
||||
@@ -54,7 +54,7 @@ fn moved_outside(from: &Mesh, onto: &Mesh, region: &Aabb3, tol: f64) -> Option<V
|
||||
/// (a slit, a moved face) and none lost, outside the tool's box.
|
||||
fn assert_local(target: &Body, tool: &Body, joined: &Body, what: &str) {
|
||||
let tol = 1e-3;
|
||||
let (mt, mtool, mj) = (target.tessellate(0.01).unwrap(), tool.tessellate(0.01).unwrap(), joined.tessellate(0.01).unwrap());
|
||||
let (mt, mtool, mj) = (target.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(), tool.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(), joined.mesh(&crate::MeshTolerance::chord(0.01)).unwrap());
|
||||
let mut region = mtool.bounds();
|
||||
region.min -= Vec3::new(0.01, 0.01, 0.01);
|
||||
region.max += Vec3::new(0.01, 0.01, 0.01);
|
||||
@@ -80,7 +80,7 @@ fn ribs_joined_flush_to_the_lid_change_nothing_else() {
|
||||
assert_local(&b, &tool, &r, &format!("{c:?}"));
|
||||
b = r;
|
||||
}
|
||||
let bounds = b.tessellate(0.01).unwrap().bounds();
|
||||
let bounds = b.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().bounds();
|
||||
assert!((bounds.max.z - 14.1).abs() < 1e-9, "the ribs keep their height: {}", bounds.max.z);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ fn a_pad_half_past_the_edge_of_its_face_fuses() {
|
||||
let (r, d) = fused(a, b);
|
||||
assert_one_solid(&r, want, 1e-6 * want, "#674");
|
||||
assert!(d.events.is_empty(), "an exact join: {d:?}");
|
||||
let bounds = r.tessellate(0.01).unwrap().bounds();
|
||||
let bounds = r.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().bounds();
|
||||
assert!((bounds.min.y + 2.5).abs() < 1e-9 && (bounds.min.z + 5.0).abs() < 0.01, "{bounds:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ fn arc_top_removed(size: f64, round: bool) -> f64 {
|
||||
/// Volume through the display planner (the mesh the app draws): closed and exact where the
|
||||
/// legacy mesher behind `measure` can leak at a zero-angle corner like the one at the tip.
|
||||
pub(crate) fn planned_volume(b: &Body) -> f64 {
|
||||
let mesh = b.display_plan(2e-4).unwrap().evaluate();
|
||||
let mesh = b.mesh_plan(&crate::MeshTolerance::chord(2e-4)).unwrap().evaluate();
|
||||
let key = |p: Vec3| [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
|
||||
let mut uses: std::collections::HashMap<([u64; 3], [u64; 3]), i32> = std::collections::HashMap::new();
|
||||
for t in &mesh.triangles {
|
||||
|
||||
@@ -38,7 +38,7 @@ fn a_barrel_between_the_sides_of_a_leaf_joins_exactly() {
|
||||
assert!(joined.validity().is_empty(), "{what}: {:?}", joined.validity());
|
||||
let inside = (x1.min(24.) - x0.max(10.)) / 4.;
|
||||
let want = 5500. + PI * r * r * (x1 - x0 - inside);
|
||||
let v = joined.tessellate(1e-4).unwrap().measure().volume;
|
||||
let v = joined.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((v - want).abs() < want * 1e-5, "{what}: volume {v} vs {want}");
|
||||
for p in joined.solid.vertex_iter().map(|v| from_p3(v.point())) {
|
||||
assert!([0., 10., 24., 40., x0, x1].iter().any(|x| (p.x - x).abs() < 1e-9), "{what}: a vertex at {p:?}");
|
||||
|
||||
@@ -41,8 +41,6 @@ mod corner_g2;
|
||||
mod curveblend;
|
||||
mod delete_face;
|
||||
mod distance;
|
||||
#[cfg(test)]
|
||||
mod edge_polyline_tests;
|
||||
mod emboss;
|
||||
mod face_params;
|
||||
#[cfg(test)]
|
||||
@@ -169,7 +167,7 @@ pub use backend::{Backend, KERNEL_ENV, backend};
|
||||
pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_max, fillet_max_each};
|
||||
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, Paint};
|
||||
pub use body::{Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, MeshTolerance, Paint};
|
||||
pub use boolean_diagnostics::{BooleanDiagnostics, BooleanFallback, BooleanStrategy, capture_boolean_diagnostics};
|
||||
pub use boundary::{Boundary, boundary_patch};
|
||||
pub use boundary_network::{BoundaryCurve, CurveSource, MAX_CHAIN_CURVES, MAX_EDGE_PICKS, boundary_join, boundary_surface};
|
||||
|
||||
@@ -26,7 +26,7 @@ struct Piece {
|
||||
/// The planar face of `body` at `at`: its index and outward normal.
|
||||
fn face_at(body: &Body, at: Vec3) -> Result<(usize, Vec3)> {
|
||||
let size = body.size();
|
||||
let mesh = body.tessellate((size * 1e-3).max(1e-3))?;
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord((size * 1e-3).max(1e-3)))?;
|
||||
let near = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
|
||||
@@ -25,7 +25,7 @@ fn loft_regions_preserve_a_tapered_round_passage_and_step() {
|
||||
for secs in [secs.to_vec(), secs.into_iter().rev().collect()] {
|
||||
let body = loft_regions(&secs).unwrap();
|
||||
check(&body, want, 1e-3);
|
||||
assert!(!body.tessellate(0.001).unwrap().contains(Vec3::new(0.0, 0.0, 10.0)), "passage was filled");
|
||||
assert!(!body.mesh(&crate::MeshTolerance::chord(0.001)).unwrap().contains(Vec3::new(0.0, 0.0, 10.0)), "passage was filled");
|
||||
let imported = step_import(&step_export(&[&body], "SolidCraft").unwrap()).unwrap();
|
||||
assert_eq!(imported.bodies.len(), 1);
|
||||
check(&imported.bodies[0].body, want, 1e-3);
|
||||
@@ -41,7 +41,7 @@ fn loft_regions_smooth_inner_and_outer_walls_together() {
|
||||
for secs in [secs.to_vec(), secs.into_iter().rev().collect()] {
|
||||
let b = loft_regions(&secs).unwrap();
|
||||
check(&b, 22400.0, 1e-4);
|
||||
assert!(!b.tessellate(0.001).unwrap().contains(Vec3::new(0.0, 0.0, 10.0)), "passage was filled");
|
||||
assert!(!b.mesh(&crate::MeshTolerance::chord(0.001)).unwrap().contains(Vec3::new(0.0, 0.0, 10.0)), "passage was filled");
|
||||
assert_eq!(measure(&b).unwrap().merged.faces, 10, "four outer walls, four inner walls, two caps");
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ fn loft_regions_match_holes_in_opposite_curve_orders_and_facing_planes() {
|
||||
for secs in [secs.to_vec(), secs.into_iter().rev().collect()] {
|
||||
let body = loft_regions(&secs).unwrap();
|
||||
check(&body, (1600.0 - PI * 34.0) * 20.0, 1e-3);
|
||||
let mesh = body.tessellate(0.001).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
assert!(!mesh.contains(Vec3::new(-8.0, 0.0, 10.0)));
|
||||
assert!(!mesh.contains(Vec3::new(8.0, 0.0, 10.0)));
|
||||
assert!(mesh.contains(Vec3::new(0.0, 12.0, 10.0)));
|
||||
|
||||
@@ -89,7 +89,7 @@ pub struct MassMeasure {
|
||||
pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
if let Some((pos, tris)) = b.triangle_mesh() {
|
||||
// Exact for a mesh body; topology counts are facets, sides and vertices.
|
||||
let m = b.tessellate(1.0)?;
|
||||
let m = b.mesh(&crate::MeshTolerance::chord(1.0))?;
|
||||
let mm = m.measure();
|
||||
let edges = b.mesh.as_ref().map(|m| m.edge_count()).unwrap_or(0);
|
||||
let merged =
|
||||
@@ -112,7 +112,7 @@ pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
let chosen = crate::measure_backend::choose(b, &mut m);
|
||||
let topo = match topo {
|
||||
Some(t) => t,
|
||||
None => b.tessellate((b.size() * 1e-3).max(1e-3))?,
|
||||
None => b.mesh(&crate::MeshTolerance::chord((b.size() * 1e-3).max(1e-3)))?,
|
||||
};
|
||||
Ok(BodyMeasure {
|
||||
volume: m.volume,
|
||||
@@ -150,7 +150,7 @@ pub fn measure_mass(b: &Body) -> Result<MassMeasure> {
|
||||
fn mass(b: &Body, topology: bool) -> Result<(MassMeasure, Option<Mesh>)> {
|
||||
let size = b.size();
|
||||
let coarse_tol = (size * 1e-3).max(1e-3);
|
||||
let coarse = b.tessellate_with(coarse_tol, true)?;
|
||||
let coarse = b.mesh(&crate::MeshTolerance::chord(coarse_tol))?;
|
||||
// Chord error relative to a radius sets the volume error: keep it near 3e-4 of the
|
||||
// smallest radius (thin tubes, small fillets).
|
||||
let tight = min_edge_radius(&coarse).map(|r| r * 3e-4).unwrap_or(f64::INFINITY);
|
||||
@@ -168,7 +168,7 @@ fn mass(b: &Body, topology: bool) -> Result<(MassMeasure, Option<Mesh>)> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let threads = 1;
|
||||
let made = crate::profile::timed(crate::profile::Kind::Tessellate, || {
|
||||
crate::par::map_on_threads(&jobs, threads, |&(tol, refine)| b.tessellate_with(tol, refine))
|
||||
crate::par::map_on_threads(&jobs, threads, |&(tol, refine)| b.mesh(&crate::MeshTolerance::chord(tol)))
|
||||
});
|
||||
let mut made = made.into_iter().map(|m| m.unwrap_or_else(|| Err(crate::KernelError::Internal("tessellation failed".into()))));
|
||||
let missing = || crate::KernelError::Internal("tessellation missing".into());
|
||||
|
||||
@@ -402,7 +402,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn mesh_of(body: &Body) -> Body {
|
||||
let mesh = body.tessellate(0.005).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.005)).unwrap();
|
||||
crate::mesh_body(&mesh.positions, &mesh.triangles).unwrap()
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ mod tests {
|
||||
assert!(!converted.is_mesh());
|
||||
assert!(converted.validity().is_empty());
|
||||
let expected = (1000. - std::f64::consts::PI * 16.) * 15.;
|
||||
let fine_volume = |body: &Body| body.tessellate_with(1e-5, true).unwrap().measure().volume;
|
||||
let fine_volume = |body: &Body| body.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap().measure().volume;
|
||||
assert!((fine_volume(&converted) - expected).abs() < 0.02);
|
||||
assert!((crate::measure(&converted).unwrap().volume - crate::measure(&source).unwrap().volume).abs() < 1e-7);
|
||||
assert_eq!(crate::measure(&converted).unwrap().merged.face_types.get("cylinder"), Some(&1));
|
||||
|
||||
@@ -38,8 +38,8 @@ fn regions_cut_from_a_body_keeping_its_meshes_are_unchanged() {
|
||||
assert_eq!(memo.shells.lock().unwrap().len(), 1, "one tolerance, one meshing");
|
||||
assert!(!memo.made.lock().unwrap().is_empty());
|
||||
// A mesh from the memo is the mesh made afresh.
|
||||
let (a, b) = (kept.tessellate_with(0.05, true).unwrap(), plate.tessellate_with(0.05, true).unwrap());
|
||||
let (a2, _) = (kept.tessellate_with(0.05, true).unwrap(), ());
|
||||
let (a, b) = (kept.mesh(&crate::MeshTolerance::chord(0.05)).unwrap(), plate.mesh(&crate::MeshTolerance::chord(0.05)).unwrap());
|
||||
let (a2, _) = (kept.mesh(&crate::MeshTolerance::chord(0.05)).unwrap(), ());
|
||||
for m in [&a, &a2] {
|
||||
assert_eq!(m.triangles, b.triangles);
|
||||
assert!(
|
||||
@@ -58,8 +58,8 @@ fn a_copy_with_another_solid_makes_its_own_meshes() {
|
||||
let other = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap();
|
||||
let mut swapped = kept.clone();
|
||||
swapped.solid = other.solid.clone();
|
||||
let _ = kept.tessellate_with(0.5, true).unwrap();
|
||||
let m = swapped.tessellate_with(0.5, true).unwrap();
|
||||
assert_eq!(m.triangles.len(), other.tessellate_with(0.5, true).unwrap().triangles.len());
|
||||
let _ = kept.mesh(&crate::MeshTolerance::chord(0.5)).unwrap();
|
||||
let m = swapped.mesh(&crate::MeshTolerance::chord(0.5)).unwrap();
|
||||
assert_eq!(m.triangles.len(), other.mesh(&crate::MeshTolerance::chord(0.5)).unwrap().triangles.len());
|
||||
assert!(swapped.boolean_meshes(0.01).is_none());
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ fn edge_at(b: &Body, p: Vec3) -> usize {
|
||||
|
||||
/// The index of the face of `b` holding `p`.
|
||||
fn face_at(b: &Body, p: Vec3) -> usize {
|
||||
let m = b.tessellate(TOL).unwrap();
|
||||
let m = b.mesh(&crate::MeshTolerance::chord(TOL)).unwrap();
|
||||
(0..b.face_count()).min_by(|x, y| to_face(&m, *x, p).total_cmp(&to_face(&m, *y, p))).unwrap()
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ fn mirrored_copy_keeps_every_face_and_edge() {
|
||||
let h = r.history().unwrap();
|
||||
h.check(&r, &[&b]).unwrap();
|
||||
// Each edge is the mirror image of the one it says it is.
|
||||
let (rm, bm) = (r.tessellate(TOL).unwrap(), b.tessellate(TOL).unwrap());
|
||||
let (rm, bm) = (r.mesh(&crate::MeshTolerance::chord(TOL)).unwrap(), b.mesh(&crate::MeshTolerance::chord(TOL)).unwrap());
|
||||
for (j, e) in h.edges.iter().enumerate() {
|
||||
let [EdgeOrigin::Edge { edge, .. }] = e.origins[..] else { panic!("edge {j}: {e:?}") };
|
||||
for p in &rm.edges[j] {
|
||||
@@ -599,7 +599,7 @@ mod smlib {
|
||||
let t = offset_backend::thicken(&s, 2.0, false).unwrap();
|
||||
assert!((volume(&t) - 200.0).abs() < 1e-6, "{}", volume(&t));
|
||||
// Along the sheet's normal (+Z).
|
||||
let m = t.tessellate(TOL).unwrap();
|
||||
let m = t.mesh(&crate::MeshTolerance::chord(TOL)).unwrap();
|
||||
let (lo, hi) = m.positions.iter().fold((f64::MAX, f64::MIN), |(lo, hi), p| (lo.min(p.z), hi.max(p.z)));
|
||||
assert!(lo.abs() < 1e-9 && (hi - 2.0).abs() < 1e-9, "z {lo}..{hi}");
|
||||
let h = verified(&t, &s, reach(2.0));
|
||||
@@ -607,7 +607,7 @@ mod smlib {
|
||||
assert_eq!(with(h, &[FaceOrigin::Offset { operand: 0, face: 0 }]).len(), 1, "{h:?}");
|
||||
assert_eq!(blends(h).len(), 4, "{h:?}");
|
||||
let both = offset_backend::thicken(&s, 2.0, true).unwrap();
|
||||
let m = both.tessellate(TOL).unwrap();
|
||||
let m = both.mesh(&crate::MeshTolerance::chord(TOL)).unwrap();
|
||||
let (lo, hi) = m.positions.iter().fold((f64::MAX, f64::MIN), |(lo, hi), p| (lo.min(p.z), hi.max(p.z)));
|
||||
assert!((lo + 1.0).abs() < 1e-9 && (hi - 1.0).abs() < 1e-9, "z {lo}..{hi}");
|
||||
let h = verified(&both, &s, reach(1.0));
|
||||
|
||||
@@ -300,7 +300,7 @@ fn smlib_hole_rim_fillet_decodes_with_its_volume() {
|
||||
);
|
||||
record.check_geometry().unwrap();
|
||||
let body = crate::native::body_from_smlib(&rim).unwrap();
|
||||
let mesh = body.tessellate(0.02).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.02)).unwrap();
|
||||
for f in 0..body.face_count() {
|
||||
assert!(mesh.tri_face.iter().any(|&t| t as usize == f), "face {f} meshes");
|
||||
}
|
||||
@@ -343,7 +343,7 @@ fn smlib_spheres_cut_beside_or_across_their_seam_decode_with_their_volume() {
|
||||
.collect();
|
||||
let loaded: Vec<bool> = body.solid.boundaries().iter().flat_map(|sh| sh.face_iter().map(|f| f.orientation()).collect::<Vec<_>>()).collect();
|
||||
assert_eq!(loaded, forward, "{name}: the body was turned inside out");
|
||||
let mesh = body.tessellate(0.02).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(0.02)).unwrap();
|
||||
for f in 0..body.face_count() {
|
||||
assert!(mesh.tri_face.iter().any(|&t| t as usize == f), "{name}: face {f} meshes");
|
||||
}
|
||||
@@ -392,7 +392,7 @@ fn smlib_torus_decodes_without_meshing_and_plans_with_a_sphere_beside_it() {
|
||||
crate::body::ORIENTED.with(|n| n.set(0));
|
||||
let body = crate::native::body_from_smlib(&s).unwrap();
|
||||
assert_eq!(crate::body::ORIENTED.with(|n| n.get()), 0, "{name}: the decode meshed the body to orient it");
|
||||
let plan = body.display_plan(0.05).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let plan = body.mesh_plan(&crate::MeshTolerance::chord(0.05)).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(plan.faces.len(), body.face_count(), "{name}");
|
||||
let ours = crate::measure(&body).unwrap().volume;
|
||||
assert!((ours - exact).abs() < 1e-3 * exact, "{name}: decoded volume {ours}, exact {exact}");
|
||||
|
||||
@@ -156,7 +156,7 @@ pub(crate) fn draft(b: &Body, at: &[Vec3], neutral: &solidcraft_geom::Plane, pul
|
||||
/// The face of `b` under each point: the face of the nearest triangle, within the picking reach.
|
||||
fn faces_under(b: &Body, at: &[Vec3]) -> Result<Vec<usize>> {
|
||||
let reach = pick_tolerance(b);
|
||||
let mesh = b.tessellate((b.size() * 1e-3).max(1e-3))?;
|
||||
let mesh = b.mesh(&crate::MeshTolerance::chord((b.size() * 1e-3).max(1e-3)))?;
|
||||
let mut out: Vec<usize> = Vec::new();
|
||||
for p in at {
|
||||
let near = mesh
|
||||
@@ -179,7 +179,7 @@ fn faces_under(b: &Body, at: &[Vec3]) -> Result<Vec<usize>> {
|
||||
|
||||
/// One point inside each face `faces` of `b`: the centroid of its largest triangle.
|
||||
fn face_points(b: &Body, faces: &[usize]) -> Result<Vec<Vec3>> {
|
||||
let mesh = b.tessellate((b.size() * 1e-3).max(1e-3))?;
|
||||
let mesh = b.mesh(&crate::MeshTolerance::chord((b.size() * 1e-3).max(1e-3)))?;
|
||||
faces
|
||||
.iter()
|
||||
.map(|f| {
|
||||
@@ -205,8 +205,8 @@ fn area([a, b, c]: &[Vec3; 3]) -> f64 {
|
||||
/// where its faces meet. `None` where a face cannot be measured.
|
||||
fn with_offsets(result: &Body, input: &Body, mut h: History, distance: f64) -> Option<History> {
|
||||
let chord = (input.size().max(result.size()) * 1e-3).min(distance / 8.0).max(1e-6);
|
||||
let source = input.tessellate(chord).ok()?;
|
||||
let made = result.tessellate(chord).ok()?;
|
||||
let source = input.mesh(&crate::MeshTolerance::chord(chord)).ok()?;
|
||||
let made = result.mesh(&crate::MeshTolerance::chord(chord)).ok()?;
|
||||
let at = |mesh: &solidcraft_geom::Mesh, face: usize| -> Vec<[Vec3; 3]> {
|
||||
mesh.triangles.iter().zip(&mesh.tri_face).filter(|(_, t)| **t as usize == face).filter_map(|(t, _)| mesh.tri(t)).collect()
|
||||
};
|
||||
|
||||
+18
-18
@@ -668,16 +668,16 @@ mod shortcut_tests {
|
||||
fn curved_near_contact_never_uses_a_shell_only_boolean_candidate() {
|
||||
let direction = Vec3::new(1.0, 2.0, 3.0).normalized().unwrap();
|
||||
let outer = crate::sphere(Vec3::ZERO, 10.0).unwrap();
|
||||
let mesh = outer.tessellate(0.03).unwrap();
|
||||
let mesh = outer.mesh(&crate::MeshTolerance::chord(0.03)).unwrap();
|
||||
for distance in [19.999, 20.001] {
|
||||
let other = crate::sphere(direction * distance, 10.0).unwrap();
|
||||
let other_mesh = other.tessellate(0.03).unwrap();
|
||||
let other_mesh = other.mesh(&crate::MeshTolerance::chord(0.03)).unwrap();
|
||||
let counts = record::counts(&[&outer, &other]);
|
||||
assert!(trivial(&outer, &other, BoolOp::Union, &mesh, &other_mesh, &counts, 0.09).is_none());
|
||||
assert!(trivial(&other, &outer, BoolOp::Union, &other_mesh, &mesh, &counts, 0.09).is_none());
|
||||
}
|
||||
let protruding = crate::sphere(direction * 9.001, 1.0).unwrap();
|
||||
let protruding_mesh = protruding.tessellate(0.03).unwrap();
|
||||
let protruding_mesh = protruding.mesh(&crate::MeshTolerance::chord(0.03)).unwrap();
|
||||
let counts = record::counts(&[&outer, &protruding]);
|
||||
assert!(trivial(&outer, &protruding, BoolOp::Cut, &mesh, &protruding_mesh, &counts, 0.09).is_none());
|
||||
}
|
||||
@@ -719,7 +719,7 @@ mod shortcut_tests {
|
||||
// Off the axis by 0.01 mm the pin shares a crescent with the tube: not taken for apart.
|
||||
let (ma, mb) = (pin(Vec3::new(0.01, 0.0, 0.0), -2.35, 9.65), tube.clone());
|
||||
let counts = record::counts(&[&ma, &mb]);
|
||||
let (sa, sb) = (ma.tessellate_with(0.009, true).unwrap(), mb.tessellate_with(0.009, true).unwrap());
|
||||
let (sa, sb) = (ma.mesh(&crate::MeshTolerance::chord(0.009)).unwrap(), mb.mesh(&crate::MeshTolerance::chord(0.009)).unwrap());
|
||||
assert!(trivial(&ma, &mb, BoolOp::Intersect, &sa, &sb, &counts, 0.027).is_none_or(|(_, sure)| !sure));
|
||||
}
|
||||
|
||||
@@ -795,7 +795,7 @@ mod shortcut_tests {
|
||||
let tube = boolean(&boolean(&outer, &wide, BoolOp::Cut).unwrap().unwrap(), &narrow, BoolOp::Cut).unwrap().unwrap();
|
||||
let pin = crate::cylinder(Vec3::new(0.0, 0.0, -2.0), Vec3::Z, 3.0, 11.05).unwrap();
|
||||
let counts = record::counts(&[&pin, &tube]);
|
||||
let (ma, mb) = (pin.tessellate_with(0.009, true).unwrap(), tube.tessellate_with(0.009, true).unwrap());
|
||||
let (ma, mb) = (pin.mesh(&crate::MeshTolerance::chord(0.009)).unwrap(), tube.mesh(&crate::MeshTolerance::chord(0.009)).unwrap());
|
||||
for op in [BoolOp::Intersect, BoolOp::Cut] {
|
||||
assert!(trivial(&pin, &tube, op, &ma, &mb, &counts, 0.027).is_none_or(|(_, sure)| !sure), "{op:?}");
|
||||
}
|
||||
@@ -816,7 +816,7 @@ mod shortcut_tests {
|
||||
let pin = crate::cylinder(Vec3::new(0.0, 0.0, -3.0), Vec3::Z, r, length + 6.0).unwrap();
|
||||
// Faceted as the boolean does for bodies of this size (0.015 mm deep crossings seen).
|
||||
let counts = record::counts(&[&pin, &tube]);
|
||||
let (ma, mb) = (pin.tessellate_with(0.005, true).unwrap(), tube.tessellate_with(0.005, true).unwrap());
|
||||
let (ma, mb) = (pin.mesh(&crate::MeshTolerance::chord(0.005)).unwrap(), tube.mesh(&crate::MeshTolerance::chord(0.005)).unwrap());
|
||||
for op in [BoolOp::Intersect, BoolOp::Cut] {
|
||||
assert!(trivial(&pin, &tube, op, &ma, &mb, &counts, 0.015).is_none_or(|(_, sure)| !sure), "{op:?}");
|
||||
}
|
||||
@@ -835,7 +835,7 @@ mod shortcut_tests {
|
||||
let a = cube(0.0, 0.0);
|
||||
// A 0.01 slab between parallel faces (0.1 % of each cube), and a 0.02 x 0.02 corner.
|
||||
for (b, want) in [(cube(9.99, 0.0), 1.0), (cube(9.98, 9.98), 0.004)] {
|
||||
let (ma, mb) = (a.tessellate(0.001).unwrap(), b.tessellate(0.001).unwrap());
|
||||
let (ma, mb) = (a.mesh(&crate::MeshTolerance::chord(0.001)).unwrap(), b.mesh(&crate::MeshTolerance::chord(0.001)).unwrap());
|
||||
let counts = record::counts(&[&a, &b]);
|
||||
for op in [BoolOp::Intersect, BoolOp::Cut, BoolOp::Union] {
|
||||
assert!(trivial(&a, &b, op, &ma, &mb, &counts, 0.003).is_none_or(|(_, sure)| !sure), "{op:?} {want}");
|
||||
@@ -847,10 +847,10 @@ mod shortcut_tests {
|
||||
}
|
||||
// Touching on a face, and one cube well inside the other, still take the shortcut.
|
||||
let (touching, inner) = (cube(10.0, 0.0), crate::box_solid(Vec3::new(2.0, 2.0, 2.0), Vec3::new(8.0, 8.0, 8.0)).unwrap());
|
||||
let ma = a.tessellate(0.001).unwrap();
|
||||
let ma = a.mesh(&crate::MeshTolerance::chord(0.001)).unwrap();
|
||||
for (b, op) in [(&touching, BoolOp::Intersect), (&inner, BoolOp::Intersect), (&inner, BoolOp::Union)] {
|
||||
let counts = record::counts(&[&a, b]);
|
||||
assert!(trivial(&a, b, op, &ma, &b.tessellate(0.001).unwrap(), &counts, 0.003).is_some_and(|(_, sure)| sure), "{op:?}");
|
||||
assert!(trivial(&a, b, op, &ma, &b.mesh(&crate::MeshTolerance::chord(0.001)).unwrap(), &counts, 0.003).is_some_and(|(_, sure)| sure), "{op:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1092,7 +1092,7 @@ fn cut_around_voids(a: &Body, b: &Body, whole: &dyn Fn(&Body, &Body, BoolOp) ->
|
||||
for sh in &shells {
|
||||
let piece =
|
||||
guard("void shell", || Body::new(Solid::try_new(vec![sh.clone()]).map_err(|e| KernelError::Failed(format!("void shell: {e}")))?))?;
|
||||
let m = piece.tessellate(tol)?;
|
||||
let m = piece.mesh(&crate::MeshTolerance::chord(tol))?;
|
||||
if m.positions.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -1106,7 +1106,7 @@ fn cut_around_voids(a: &Body, b: &Body, whole: &dyn Fn(&Body, &Body, BoolOp) ->
|
||||
};
|
||||
let Some(outer) = (0..shells.len()).max_by(|i, j| extent(*i).total_cmp(&extent(*j))) else { return Ok(None) };
|
||||
let Some(ob) = boxes.get(outer) else { return Ok(None) };
|
||||
let tm = b.tessellate((b.size() * 1e-3).max(1e-3))?;
|
||||
let tm = b.mesh(&crate::MeshTolerance::chord((b.size() * 1e-3).max(1e-3)))?;
|
||||
let tb = tm.bounds();
|
||||
// Clear of the tool by more than both meshes' deflection.
|
||||
let gap = 4.0 * tol + (b.size() * 1e-3).max(1e-3);
|
||||
@@ -1375,7 +1375,7 @@ where
|
||||
return Ok(Some(body.clone()));
|
||||
}
|
||||
}
|
||||
let mut meshes = (a.tessellate_with(tol_m, true).ok(), b.tessellate_with(tol_m, true).ok());
|
||||
let mut meshes = (a.mesh(&crate::MeshTolerance::chord(tol_m)).ok(), b.mesh(&crate::MeshTolerance::chord(tol_m)).ok());
|
||||
// The vertex boxes above miss a curved operand's reach (a cylinder has only seam vertices),
|
||||
// so a thin meeting region of curved bodies kept the coarse step: a sliver 0.5 mm thick was
|
||||
// faceted at 0.017 mm and a correct result failed the membership test. The meshes just made
|
||||
@@ -1393,7 +1393,7 @@ where
|
||||
if let Some(refined) = refined
|
||||
&& refined < tol_m * 0.5
|
||||
{
|
||||
let again = (a.tessellate_with(refined, true).ok(), b.tessellate_with(refined, true).ok());
|
||||
let again = (a.mesh(&crate::MeshTolerance::chord(refined)).ok(), b.mesh(&crate::MeshTolerance::chord(refined)).ok());
|
||||
if again.0.is_some() && again.1.is_some() {
|
||||
tol_m = refined;
|
||||
meshes = again;
|
||||
@@ -1428,7 +1428,7 @@ where
|
||||
reject(reason);
|
||||
return (false, f64::NAN);
|
||||
}
|
||||
let Ok(mr) = r.tessellate_with(tol_m, true) else { return (false, f64::NAN) };
|
||||
let Ok(mr) = r.mesh(&crate::MeshTolerance::chord(tol_m)) else { return (false, f64::NAN) };
|
||||
let v = mr.measure().volume;
|
||||
let bounds = plausible(v, op, va, vb, slack);
|
||||
if !bounds {
|
||||
@@ -1486,7 +1486,7 @@ where
|
||||
// Measured as the operands were: a coarser mesh of a plate with many holes
|
||||
// overstates its volume by more than the slack.
|
||||
let v = match &meshes {
|
||||
(Some(_), Some(_)) => body.tessellate_with(tol_m, true).map(|m| m.measure().volume).unwrap_or(f64::NAN),
|
||||
(Some(_), Some(_)) => body.mesh(&crate::MeshTolerance::chord(tol_m)).map(|m| m.measure().volume).unwrap_or(f64::NAN),
|
||||
_ => volume(&body),
|
||||
};
|
||||
if plausible(v, op, va, vb, slack) {
|
||||
@@ -1983,7 +1983,7 @@ const SCREWS: [([f64; 3], [f64; 3]); 3] =
|
||||
/// length): the slivers a nudged tool leaves where it was pushed into a face it touched.
|
||||
fn sliver_faces(b: &Body, size: f64) -> usize {
|
||||
use std::collections::HashMap;
|
||||
let Ok(m) = b.tessellate((size * 1e-3).max(1e-4)) else { return usize::MAX };
|
||||
let Ok(m) = b.mesh(&crate::MeshTolerance::chord((size * 1e-3).max(1e-4))) else { return usize::MAX };
|
||||
let q = (size * 1e-7).max(1e-12);
|
||||
let key = |p: Vec3| ((p.x / q).round() as i64, (p.y / q).round() as i64, (p.z / q).round() as i64);
|
||||
// Per face: its area, and how many of its triangles use each mesh edge (its boundary once).
|
||||
@@ -2053,7 +2053,7 @@ pub(crate) fn truck_boolean(a: &Body, b: &Body, op: BoolOp, shift: Option<mt::Ve
|
||||
/// A boolean of `a` and `b` given as their pieces (`la`, `lb`); the result's pieces in one body.
|
||||
pub(crate) fn by_lumps(a: &Body, b: &Body, la: Vec<Body>, lb: Vec<Body>, op: BoolOp) -> Result<Option<Body>> {
|
||||
let boxes_meet = |x: &Body, y: &Body| -> bool {
|
||||
let (Ok(mx), Ok(my)) = (x.tessellate(x.size() * 0.05 + 1e-3), y.tessellate(y.size() * 0.05 + 1e-3)) else { return true };
|
||||
let (Ok(mx), Ok(my)) = (x.mesh(&crate::MeshTolerance::chord(x.size() * 0.05 + 1e-3)), y.mesh(&crate::MeshTolerance::chord(y.size() * 0.05 + 1e-3))) else { return true };
|
||||
let (bx, by) = (mx.bounds(), my.bounds());
|
||||
let g = (x.size() + y.size()) * 0.01;
|
||||
bx.min.x <= by.max.x + g
|
||||
@@ -2139,7 +2139,7 @@ fn union_apart_untimed(bodies: &[Body]) -> Result<Option<Body>> {
|
||||
// A coarse mesh is enough: the chord tolerance bounds how far the true surface lies
|
||||
// outside it, and the box grows by twice that.
|
||||
let tol = (b.size() * 0.02).max(1e-3);
|
||||
let bb = b.tessellate(tol)?.bounds();
|
||||
let bb = b.mesh(&crate::MeshTolerance::chord(tol))?.bounds();
|
||||
if bb.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ fn a_lip_swept_on_a_round_rim_joins_it() {
|
||||
for inset in [0.0, 0.5] {
|
||||
let (r0, r1) = (9.0 - inset, 10.0 - inset);
|
||||
// Measured as the result is (the round faces' volume from their meshes).
|
||||
let mesh_volume = |b: &Body| b.tessellate(1e-5).unwrap().measure().volume;
|
||||
let mesh_volume = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap().measure().volume;
|
||||
let want = mesh_volume(&body) + mesh_volume(&lip(inset));
|
||||
assert!((want - PI * 1000.0 - PI * (r1 * r1 - r0 * r0) / 4.0).abs() < 0.05);
|
||||
for (a, b) in [(&body, &lip(inset)), (&lip(inset), &body)] {
|
||||
@@ -43,7 +43,7 @@ fn a_lip_swept_on_a_round_rim_joins_it() {
|
||||
fn a_swept_lips_foot_is_a_plane() {
|
||||
let l = lip(0.0);
|
||||
let foot = crate::coplanar::faces_in(&l, -Vec3::Z, -10.0, 1e-6);
|
||||
let m = l.tessellate(1e-5).unwrap();
|
||||
let m = l.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
let area: f64 = m
|
||||
.triangles
|
||||
.iter()
|
||||
|
||||
@@ -24,7 +24,7 @@ fn disc(cx: f64, cy: f64, r: f64) -> Loop2 {
|
||||
}
|
||||
|
||||
fn assert_solid(r: &Body, volume: f64, rel: f64, shells: usize, what: &str) {
|
||||
let v = r.tessellate(1e-4).unwrap().measure().volume;
|
||||
let v = r.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((v - volume).abs() <= volume * rel, "{what}: volume {v} vs {volume}");
|
||||
assert_eq!(r.shell_count(), shells, "{what}: shells");
|
||||
assert!(r.validity().is_empty(), "{what}: {:?}", r.validity());
|
||||
@@ -174,7 +174,7 @@ fn stepped_cuts_touching_along_a_line_keep_their_pieces() {
|
||||
post(x) * dx * if x < 25.0 { 20.0 } else { 10.0 }
|
||||
})
|
||||
.sum();
|
||||
let v = res.tessellate(1e-4).unwrap().measure().volume;
|
||||
let v = res.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
|
||||
assert!((v - volume).abs() <= volume * 1e-4, "posts: volume {v} vs {volume}");
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}");
|
||||
let sewn = crate::prism::prism_boolean(&stepped, &tool, BoolOp::Cut);
|
||||
|
||||
@@ -72,7 +72,7 @@ fn periodic_step_seam_survives_identity_rotation_and_translation() {
|
||||
let source_vertices: Vec<_> = body.solid.vertex_iter().map(|v| from_p3(v.point())).collect();
|
||||
let source_faces = body.solid.face_iter().count();
|
||||
let unchanged = crate::step_export(&[body], "source snapshot").unwrap();
|
||||
let before = body.tessellate_with(1e-3, true).unwrap().measure().volume;
|
||||
let before = body.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().measure().volume;
|
||||
assert!((before - 2000. * std::f64::consts::PI).abs() < 1.);
|
||||
for matrix in [
|
||||
[[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]],
|
||||
@@ -83,11 +83,11 @@ fn periodic_step_seam_survives_identity_rotation_and_translation() {
|
||||
assert_topology(body, &moved, matrix);
|
||||
assert_eq!(moved.solid.face_iter().count(), source_faces);
|
||||
assert_eq!(moved.solid.vertex_iter().count(), source_vertices.len());
|
||||
let after = moved.tessellate_with(1e-3, true).unwrap().measure().volume;
|
||||
let after = moved.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().measure().volume;
|
||||
assert!((after - before).abs() < 1e-5, "{after} vs {before}");
|
||||
let roundtrip = crate::step_import(&crate::step_export(&[&moved], "placement regression").unwrap()).unwrap();
|
||||
assert!(roundtrip.warnings.is_empty(), "{:?}", roundtrip.warnings);
|
||||
assert!((roundtrip.bodies.first().unwrap().body.tessellate_with(1e-3, true).unwrap().measure().volume - before).abs() < 1e-4);
|
||||
assert!((roundtrip.bodies.first().unwrap().body.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().measure().volume - before).abs() < 1e-4);
|
||||
}
|
||||
let rigid = transform(body, Vec3::new(10., 10., 30.), Vec3::ZERO, Vec3::Z, std::f64::consts::FRAC_PI_2).unwrap();
|
||||
assert_topology(body, &rigid, [[0., 1., 0., 0.], [-1., 0., 0., 0.], [0., 0., 1., 0.], [10., 10., 30., 1.]]);
|
||||
|
||||
@@ -52,7 +52,7 @@ fn plane_faces(b: &Body) -> usize {
|
||||
}
|
||||
|
||||
fn volume(b: &Body) -> f64 {
|
||||
b.tessellate_with(1e-3, true).unwrap().measure().volume
|
||||
b.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().measure().volume
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -127,7 +127,7 @@ fn join_checked(a: &Body, b: &Body, walls: &[(f64, f64)], turn: f64) {
|
||||
// Decided in 2D: the walls come back as arcs, not as splines.
|
||||
assert_eq!(free_form_faces(&joined), 0);
|
||||
let want = HEIGHT * union_plan_area(walls, turn);
|
||||
let got = joined.tessellate_with(1e-2, true).unwrap().measure().volume;
|
||||
let got = joined.mesh(&crate::MeshTolerance::chord(1e-2)).unwrap().measure().volume;
|
||||
assert!((got - want).abs() < 5e-3 * want, "{got} vs {want}");
|
||||
}
|
||||
|
||||
|
||||
@@ -379,8 +379,8 @@ mod tests {
|
||||
assert!((moved.face_plane(floor).unwrap().origin.z - (300. + thickness)).abs() < 1e-9);
|
||||
assert!(matches!(moved.face_sources().unwrap().get(right), Some(Some(FaceSource::ShellPlanar { producer: Some(9), inner: false, .. }))));
|
||||
assert!(moved.face_plane(right).unwrap().normal().dot(Vec3::Y) > 0.999999);
|
||||
let raw = cup.tessellate(0.01).unwrap();
|
||||
let display = cup.display_mesh(0.01).unwrap();
|
||||
let raw = cup.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
let display = cup.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
let cylindrical: Vec<_> = cup
|
||||
.solid
|
||||
.face_iter()
|
||||
@@ -429,7 +429,7 @@ mod tests {
|
||||
assert!((rv - dv).abs() <= raw.measure().area * 0.01, "display volume {dv} vs {rv}");
|
||||
let expected = (width * 60. - (4. - std::f64::consts::PI) * 36.) * 30.
|
||||
- ((width - 2. * thickness) * (60. - 2. * thickness) - (4. - std::f64::consts::PI) * (6. - thickness).powi(2)) * (30. - thickness);
|
||||
assert!((cup.tessellate(1e-4).unwrap().measure().volume - expected).abs() < 0.2);
|
||||
assert!((cup.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume - expected).abs() < 0.2);
|
||||
let imported = crate::step_import(&crate::step_export(&[&cup], "regularized rounded shell").unwrap()).unwrap();
|
||||
assert!(imported.warnings.is_empty());
|
||||
let copy = &imported.bodies[0].body;
|
||||
@@ -447,7 +447,7 @@ mod tests {
|
||||
assert!(z.iter().any(|z| z.abs() < 1e-8));
|
||||
assert!(z.iter().any(|z| (*z - 30.).abs() < 1e-8));
|
||||
}
|
||||
let copy_mesh = copy.tessellate(1e-4).unwrap();
|
||||
let copy_mesh = copy.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap();
|
||||
assert!((copy_mesh.measure().volume - expected).abs() < 0.2);
|
||||
assert!(copy_mesh.contains(Vec3::new(width / 2. - thickness / 2., 0., 15.)));
|
||||
assert!(!copy_mesh.contains(Vec3::new(0., 0., 15.)));
|
||||
@@ -459,7 +459,7 @@ mod tests {
|
||||
let pending = shell(&source, &[Vec3::new(20., 15., 20.)], 2.).unwrap();
|
||||
let cup = pending.clone().with_shell_certificate(2).unwrap();
|
||||
assert_eq!(cup.solid.face_iter().count(), 11);
|
||||
let mesh = cup.tessellate(0.01).unwrap();
|
||||
let mesh = cup.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
assert!((mesh.measure().volume - (40. * 30. * 20. - 36. * 26. * 18.)).abs() < 1e-6);
|
||||
assert!(mesh.contains(Vec3::new(20., 1., 1.)));
|
||||
assert!(mesh.contains(Vec3::new(20., 1., 10.)));
|
||||
@@ -468,7 +468,7 @@ mod tests {
|
||||
assert!(imported.warnings.is_empty());
|
||||
let copy = &imported.bodies[0].body;
|
||||
assert_eq!(copy.solid.face_iter().count(), 11);
|
||||
assert!((copy.tessellate(0.01).unwrap().measure().volume - mesh.measure().volume).abs() < 1e-6);
|
||||
assert!((copy.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().volume - mesh.measure().volume).abs() < 1e-6);
|
||||
}
|
||||
#[test]
|
||||
fn union_keeps_unchanged_supports_of_both_operands_and_drops_shared_ones() {
|
||||
@@ -551,8 +551,8 @@ mod tests {
|
||||
let (x, y) = (24.73317426966596, 21.733174269665962);
|
||||
let cutter = box_solid(Vec3::new(-x, -y, 1.), Vec3::new(x, y, 10.)).unwrap();
|
||||
let pocket = crate::boolean(&original, &cutter, crate::BoolOp::Cut).unwrap().unwrap();
|
||||
let raw = pocket.tessellate(0.01).unwrap();
|
||||
let display = pocket.display_mesh(0.01).unwrap();
|
||||
let raw = pocket.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
let display = pocket.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
let sources = pocket.face_sources().unwrap();
|
||||
assert_eq!(pocket.solid.face_iter().count(), 11, "four outer walls, four inner walls, rim, floor, bottom");
|
||||
let sides: Vec<_> =
|
||||
@@ -575,7 +575,7 @@ mod tests {
|
||||
assert!(imported.warnings.is_empty());
|
||||
let copy = &imported.bodies[0].body;
|
||||
assert_eq!(copy.solid.face_iter().count(), 11);
|
||||
let copy_mesh = copy.tessellate(0.01).unwrap();
|
||||
let copy_mesh = copy.mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
|
||||
assert!((copy_mesh.measure().volume - raw.measure().volume).abs() < 1e-6);
|
||||
assert!(copy_mesh.contains(Vec3::new(0., 0., 0.5)));
|
||||
assert!(!copy_mesh.contains(Vec3::new(0., 0., 5.)));
|
||||
|
||||
@@ -287,7 +287,7 @@ fn planar_polys(b: &Body, operand: usize) -> std::result::Result<Vec<Poly>, NotP
|
||||
let tol = (b.size() * 1e-3).max(1e-3);
|
||||
let unmeshed = |e: KernelError| NotPlanar::Other(format!("faces could not be meshed ({e})"));
|
||||
let faces = b.faces(tol).map_err(unmeshed)?;
|
||||
let mesh = b.tessellate(tol).map_err(unmeshed)?;
|
||||
let mesh = b.mesh(&crate::MeshTolerance::chord(tol)).map_err(unmeshed)?;
|
||||
let planes = face_planes(b, &faces)?;
|
||||
if mesh.triangles.len() > MAX_POLYS {
|
||||
return Err(NotPlanar::Other(format!("mesh is too detailed ({} triangles)", mesh.triangles.len())));
|
||||
@@ -312,7 +312,7 @@ fn planar_polys(b: &Body, operand: usize) -> std::result::Result<Vec<Poly>, NotP
|
||||
/// pieces of its faces as operand `operand`.
|
||||
fn faceted_polys(b: &Body, tol: f64, operand: usize) -> Option<Vec<Poly>> {
|
||||
let faces = b.faces(tol).ok()?;
|
||||
let mesh = b.tessellate(tol).ok()?;
|
||||
let mesh = b.mesh(&crate::MeshTolerance::chord(tol)).ok()?;
|
||||
let planes: Vec<Option<PlaneEq>> = faces.iter().map(|f| f.plane_normal.map(|n| PlaneEq { n, w: n.dot(f.centroid) })).collect();
|
||||
let mut out = Vec::new();
|
||||
for (t, fi) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
@@ -465,7 +465,7 @@ pub(crate) fn sliced_planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Re
|
||||
let size = a.size().max(b.size());
|
||||
let tol = (a.size() * 1e-3).max(1e-3);
|
||||
let infos = a.faces(tol).ok()?;
|
||||
let mesh = a.tessellate(tol).ok()?;
|
||||
let mesh = a.mesh(&crate::MeshTolerance::chord(tol)).ok()?;
|
||||
// Each face's plane normal where it is planar (see [`curved_kind`]): a small round meshed
|
||||
// flat is no plane, and taken for one the middle piece would rebuild it as a chamfer.
|
||||
let surface_tol = kind_tol(a);
|
||||
|
||||
@@ -296,7 +296,7 @@ pub fn draft(b: &Body, faces: &[Vec3], neutral: &Plane, pull: Vec3, angle: f64)
|
||||
fn draft_walls(b: &Body, at: &[Vec3], neutral: &Plane, pull: Vec3, angle: f64) -> Result<Body> {
|
||||
let healed = crate::boolean_record::healed(b)?;
|
||||
let size = healed.size();
|
||||
let mesh = healed.tessellate((size * 1e-3).max(1e-3))?;
|
||||
let mesh = healed.mesh(&crate::MeshTolerance::chord((size * 1e-3).max(1e-3)))?;
|
||||
let mut chosen: Vec<usize> = Vec::new();
|
||||
for p in at {
|
||||
let near = mesh
|
||||
@@ -470,7 +470,7 @@ fn shell_planar(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
|
||||
if b.paint().is_some_and(|paint| !paint.faces.is_empty()) { crate::boolean_record::copied(b)? } else { crate::boolean_record::healed(b)? };
|
||||
let healed = healed.with_paint(b.paint().cloned()).with_split_lines(b.split_lines().cloned()).inherit_regularization_protection(b);
|
||||
let size = healed.size();
|
||||
let mesh = healed.tessellate((size * 1e-3).max(1e-3))?;
|
||||
let mesh = healed.mesh(&crate::MeshTolerance::chord((size * 1e-3).max(1e-3)))?;
|
||||
// Faces to open: the face of the triangle nearest each point.
|
||||
let mut opened: Vec<usize> = Vec::new();
|
||||
for p in open {
|
||||
@@ -672,7 +672,7 @@ fn shell_opened(b: &Body, healed: &Body, open: &[Vec3], opened: &[usize], thickn
|
||||
// Healing can leave faces the boolean dislikes (revolved patches): the body as it was,
|
||||
// its faces to open found again.
|
||||
Err(e) => {
|
||||
let raw = b.tessellate((size * 1e-3).max(1e-3)).map_err(|_| e.clone())?;
|
||||
let raw = b.mesh(&crate::MeshTolerance::chord((size * 1e-3).max(1e-3))).map_err(|_| e.clone())?;
|
||||
let mut opened_raw: Vec<usize> = Vec::new();
|
||||
for p in open {
|
||||
let near = raw
|
||||
@@ -712,7 +712,7 @@ fn opening_overreach(healed: &Body, cavity: &Body, opened: &[usize], thickness:
|
||||
let tol = size * 1e-6 + 1e-9;
|
||||
let faces: Vec<mt::Face> = healed.solid.face_iter().cloned().collect();
|
||||
// Each open face's plane (outward normal, offset) and its triangles.
|
||||
let mesh = healed.tessellate((size * 1e-3).max(1e-3))?;
|
||||
let mesh = healed.mesh(&crate::MeshTolerance::chord((size * 1e-3).max(1e-3)))?;
|
||||
let mut open: Vec<(&mt::Face, Vec3, f64, Vec<[Vec3; 3]>)> = Vec::new();
|
||||
for fi in opened {
|
||||
let Some(f) = faces.get(*fi) else { return Ok(None) };
|
||||
@@ -850,7 +850,7 @@ fn solve3(m: [[f64; 3]; 3], r: [f64; 3]) -> Option<[f64; 3]> {
|
||||
/// move, so they pick the same surface either way.
|
||||
fn faces_at(body: &Body, at: &[Vec3]) -> Result<Vec<usize>> {
|
||||
let size = body.size();
|
||||
let mesh = body.tessellate((size * 1e-3).max(1e-3))?;
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord((size * 1e-3).max(1e-3)))?;
|
||||
let mut chosen: Vec<usize> = Vec::new();
|
||||
for p in at {
|
||||
let near = mesh
|
||||
|
||||
+14
-14
@@ -2690,7 +2690,7 @@ mod planar_tests {
|
||||
let mut upper = 0;
|
||||
for lump in &lumps {
|
||||
assert!(lump.validity().is_empty());
|
||||
let mesh = lump.tessellate(1e-5).unwrap();
|
||||
let mesh = lump.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
if mesh.contains(Vec3::new(21., 21., 0.5)) {
|
||||
lower += 1;
|
||||
assert!(!mesh.contains(Vec3::new(25., 25., 0.5)));
|
||||
@@ -2765,7 +2765,7 @@ mod planar_tests {
|
||||
let shell = crate::shell(&stock, &[Vec3::new(0., 0., 24.)], 1.).unwrap();
|
||||
assert_eq!(coplanar_joins(&shell), 0, "Shell must not add internal edges across continuous planar walls");
|
||||
assert!(shell.validity().is_empty());
|
||||
let mesh = shell.tessellate(1e-5).unwrap();
|
||||
let mesh = shell.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
let expected = 40. * 30. * 24. + 8. * 10. * 8. - (38. * 28. * 23. + 6. * 10. * 6.);
|
||||
assert!((mesh.measure().volume - expected).abs() < 1e-6);
|
||||
for (point, material) in [
|
||||
@@ -2784,7 +2784,7 @@ mod planar_tests {
|
||||
let body = &reopened.bodies[0].body;
|
||||
assert_eq!(body.face_count(), shell.face_count());
|
||||
assert_eq!(coplanar_joins(body), 0);
|
||||
assert!((body.tessellate(1e-5).unwrap().measure().volume - expected).abs() < 1e-6);
|
||||
assert!((body.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap().measure().volume - expected).abs() < 1e-6);
|
||||
assert_eq!(crate::step_export(&[&stock], "unchanged stock").unwrap(), snapshot);
|
||||
}
|
||||
|
||||
@@ -2822,7 +2822,7 @@ mod planar_tests {
|
||||
}
|
||||
assert!(old.face_sources().is_none_or(|s| s.iter().all(Option::is_none)));
|
||||
let expected = if curved { std::f64::consts::PI * (100. * 20. - 25. * 18.) } else { 400. * 20. - 100. * 18. };
|
||||
let mesh = merged.tessellate(1e-5).unwrap();
|
||||
let mesh = merged.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
assert!((mesh.measure().volume - expected).abs() < 0.03);
|
||||
assert!(mesh.contains(Vec3::new(0., 0., 1.)));
|
||||
assert!(!mesh.contains(Vec3::new(0., 0., 10.)));
|
||||
@@ -2849,7 +2849,7 @@ mod planar_tests {
|
||||
// Independent circular chord-volume bound: 2*pi*(10*20+5*18)*1e-5 < .019.
|
||||
let expected = std::f64::consts::PI * (100. * 20. - 25. * 18.);
|
||||
for body in [&output, &merged] {
|
||||
let mesh = body.tessellate(1e-5).unwrap();
|
||||
let mesh = body.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
assert!((mesh.measure().volume - expected).abs() < 0.03);
|
||||
assert!(mesh.contains(Vec3::new(0., 0., 1.)));
|
||||
assert!(!mesh.contains(Vec3::new(0., 0., 10.)));
|
||||
@@ -2885,7 +2885,7 @@ mod planar_tests {
|
||||
let result = stack_boolean(&stock, &tool, BoolOp::Cut).expect("qualified overlapping stacks").unwrap().unwrap();
|
||||
assert_eq!(result.shell_count(), 1);
|
||||
assert!(result.validity().is_empty());
|
||||
let mesh = result.tessellate(1e-5).unwrap();
|
||||
let mesh = result.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
let expected = (1200.0 - 25.0 * std::f64::consts::PI) * 30.0;
|
||||
// An inscribed circular mesh at radial chord error 1e-5 has volume
|
||||
// deficit below 2*pi*R*height*error, conservatively 0.01 mm^3 here.
|
||||
@@ -2914,7 +2914,7 @@ mod planar_tests {
|
||||
let result = stack_boolean(&stock, &tool, BoolOp::Cut).expect("exact aligned cut path").unwrap().unwrap();
|
||||
assert_eq!(result.shell_count(), shells);
|
||||
assert!(result.validity().is_empty());
|
||||
let mesh = result.tessellate(1e-5).unwrap();
|
||||
let mesh = result.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
assert!((mesh.measure().volume - retained * area).abs() < 0.02);
|
||||
let at = |x: f64, y: f64, z: f64| plane.to_world(Vec2::new(x, y)) + plane.normal() * z;
|
||||
for z in [2.5, 12.5, 32.5] {
|
||||
@@ -2924,15 +2924,15 @@ mod planar_tests {
|
||||
assert_eq!(mesh.contains(at(25.2, 15.0, z)), material);
|
||||
}
|
||||
let public = crate::boolean(&stock, &tool, BoolOp::Cut).unwrap().unwrap();
|
||||
assert!((public.tessellate(1e-5).unwrap().measure().volume - retained * area).abs() < 0.02);
|
||||
assert!((public.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap().measure().volume - retained * area).abs() < 0.02);
|
||||
if lo == 30.0 && hi > 35.0 {
|
||||
let imported = crate::step_import(&crate::step_export(&[&result], "inward annular cut").unwrap()).unwrap();
|
||||
assert!(imported.warnings.is_empty());
|
||||
let saved = &imported.bodies.first().unwrap().body;
|
||||
assert!((saved.tessellate(1e-5).unwrap().measure().volume - 30.0 * area).abs() < 0.02);
|
||||
assert!((saved.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap().measure().volume - 30.0 * area).abs() < 0.02);
|
||||
let next = crate::extrude(&plane, std::slice::from_ref(®ion), 25.0, 31.0).unwrap().pop().unwrap();
|
||||
let shortened = crate::boolean(saved, &next, BoolOp::Cut).unwrap().unwrap();
|
||||
let next_volume = shortened.tessellate(1e-5).unwrap().measure().volume;
|
||||
let next_volume = shortened.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap().measure().volume;
|
||||
assert!(
|
||||
(next_volume - 25.0 * area).abs() < 0.02,
|
||||
"seam {seam}, plane {plane:?}, next cut volume {next_volume}, expected {}",
|
||||
@@ -2947,8 +2947,8 @@ mod planar_tests {
|
||||
assert!(stack_boolean(&stock, &clear, BoolOp::Cut).is_none());
|
||||
let unchanged = crate::boolean(&stock, &clear, BoolOp::Cut).unwrap().unwrap();
|
||||
assert!(std::sync::Arc::ptr_eq(&stock.solid, &unchanged.solid), "disjoint Cut must retain exact source topology");
|
||||
let unchanged_mesh = unchanged.tessellate(1e-5).unwrap();
|
||||
let source_mesh = stock.tessellate(1e-5).unwrap();
|
||||
let unchanged_mesh = unchanged.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
let source_mesh = stock.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
assert_eq!(unchanged_mesh.measure().volume, source_mesh.measure().volume);
|
||||
// tessellate() floors tolerance at body.size()*1e-6. The arc
|
||||
// polygon's area error is at most perimeter*radial chord error.
|
||||
@@ -2972,7 +2972,7 @@ mod planar_tests {
|
||||
let tool = crate::extrude(&Plane::XY, &[larger_hole], 30.0, 36.0).unwrap().pop().unwrap();
|
||||
let result = stack_boolean(&stock, &tool, BoolOp::Cut).unwrap().unwrap().unwrap();
|
||||
let expected = (1200.0 - 25.0 * std::f64::consts::PI) * 30.0 + std::f64::consts::PI * (5.1_f64.powi(2) - 25.0) * 5.0;
|
||||
let mesh = result.tessellate(1e-5).unwrap();
|
||||
let mesh = result.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap();
|
||||
assert!((mesh.measure().volume - expected).abs() < 0.02);
|
||||
assert!(mesh.contains(Vec3::new(25.05, 15.0, 32.0)));
|
||||
assert!(!mesh.contains(Vec3::new(25.2, 15.0, 32.0)));
|
||||
@@ -3035,7 +3035,7 @@ mod planar_tests {
|
||||
assert!(result.validity().is_empty());
|
||||
assert_eq!(result.shell_count(), 1);
|
||||
assert!((crate::measure(&result).unwrap().volume - 14_725.).abs() < 1e-6);
|
||||
let mesh = result.tessellate(1e-4).unwrap();
|
||||
let mesh = result.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap();
|
||||
for (point, material) in [
|
||||
(Vec3::new(22.5, 25., 0.5), false),
|
||||
(Vec3::new(22.5, 25., 2.), true),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user