From 42997a62ed70b3d8c2af22fe013ea63d25797555 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:18:36 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt --- Cargo.lock | 1 - Cargo.toml | 1 - apps/solidcraft-cli/src/kernel_cases.rs | 2 +- apps/solidcraft-cli/src/oracle.rs | 2 +- apps/solidcraft-cli/tests/action_matrix.rs | 10 +- apps/solidcraft-cli/tests/bracket_example.rs | 4 +- apps/solidcraft-cli/tests/design_corpus.rs | 2 +- .../tests/straight_edges_after_cuts.rs | 2 +- crates/doc/src/display_quality.rs | 2 +- crates/doc/src/eval.rs | 12 +- crates/doc/src/eval_more.rs | 12 +- crates/doc/src/joints_cam.rs | 2 +- crates/doc/src/placed_mesh.rs | 6 +- crates/doc/src/sheet.rs | 2 +- crates/engine/src/boolean_regression_tests.rs | 38 +- crates/engine/src/cmd/clearance.rs | 2 +- crates/engine/src/cmd/compare.rs | 2 +- crates/engine/src/cmd/component_clone.rs | 4 +- .../engine/src/cmd/component_clone_tests.rs | 20 +- .../src/cmd/config_certificate_tests.rs | 12 +- crates/engine/src/cmd/contact_sweep.rs | 2 +- crates/engine/src/cmd/edit_tests.rs | 2 +- crates/engine/src/cmd/features_more_tests.rs | 22 +- crates/engine/src/cmd/lineage_face_tests.rs | 2 +- crates/engine/src/cmd/motion.rs | 4 +- .../src/cmd/revolve_convention_tests.rs | 8 +- crates/engine/src/cmd/sheet_tests.rs | 4 +- crates/engine/src/cmd/sketch_face.rs | 2 +- crates/engine/src/cmd/sketch_face_tests.rs | 8 +- crates/engine/src/cmd/sketch_project_tests.rs | 2 +- crates/engine/src/cmd/surface.rs | 2 +- .../engine/src/cmd/surface_boundary_tests.rs | 2 +- crates/engine/src/cmd/sweep_options_tests.rs | 2 +- crates/engine/src/cmd/view_dxf_meshes.rs | 2 +- crates/engine/src/display_mesh_tests.rs | 2 +- crates/engine/src/pattern_suppress_tests.rs | 4 +- crates/engine/src/profile_curves_tests.rs | 2 +- crates/engine/src/profile_fallback_tests.rs | 2 +- crates/engine/src/profile_refs_tests.rs | 2 +- crates/engine/src/tests.rs | 2 +- .../engine/src/tests/boolean_diagnostics.rs | 2 +- crates/engine/src/tests/curved_seam_round.rs | 2 +- crates/engine/src/tests/flush_join_loft.rs | 2 +- crates/engine/src/tests/loft_ring.rs | 2 +- crates/engine/src/tests/mesh_convert.rs | 2 +- crates/engine/src/tests/robustness.rs | 6 +- crates/engine/src/thread_tests.rs | 6 +- crates/gpu/src/tests.rs | 12 +- crates/io/src/lib.rs | 2 +- crates/kernel/Cargo.toml | 4 - crates/kernel/src/blend.rs | 26 +- crates/kernel/src/blend_corner.rs | 2 +- crates/kernel/src/blend_fit.rs | 4 +- crates/kernel/src/blend_smlib.rs | 2 +- crates/kernel/src/blend_variable.rs | 2 +- crates/kernel/src/body.rs | 555 +++++------------- crates/kernel/src/body_lump_tests.rs | 24 +- crates/kernel/src/boolean_guard.rs | 6 +- crates/kernel/src/boolean_history_tests.rs | 6 +- crates/kernel/src/bore_union_tests.rs | 18 +- crates/kernel/src/boundary_network.rs | 8 +- crates/kernel/src/candidate_reach_tests.rs | 4 +- crates/kernel/src/clearance.rs | 2 +- crates/kernel/src/coaxial_overlap.rs | 4 +- crates/kernel/src/coplanar.rs | 8 +- crates/kernel/src/coplanar_tests.rs | 4 +- crates/kernel/src/curveblend_law_tests.rs | 2 +- crates/kernel/src/curveblend_tests.rs | 12 +- crates/kernel/src/curvecorner_tests.rs | 2 +- crates/kernel/src/delete_face.rs | 4 +- crates/kernel/src/display_mesh_tests.rs | 38 +- crates/kernel/src/emboss.rs | 26 +- crates/kernel/src/face_source_tests.rs | 10 +- crates/kernel/src/fill.rs | 2 +- crates/kernel/src/fill_general_tests.rs | 2 +- crates/kernel/src/helix.rs | 2 +- crates/kernel/src/history_tests.rs | 2 +- crates/kernel/src/iges_in/tests.rs | 2 +- crates/kernel/src/issue_616_tests.rs | 4 +- crates/kernel/src/issue_679_tests.rs | 2 +- crates/kernel/src/issue_851_tests.rs | 2 +- crates/kernel/src/join_between_caps_tests.rs | 2 +- crates/kernel/src/lib.rs | 4 +- crates/kernel/src/lip.rs | 2 +- crates/kernel/src/loft_region_tests.rs | 6 +- crates/kernel/src/measure.rs | 8 +- crates/kernel/src/mesh_convert.rs | 4 +- crates/kernel/src/mesh_memo_tests.rs | 10 +- crates/kernel/src/modify_history_tests.rs | 8 +- crates/kernel/src/native/adapter_tests.rs | 6 +- crates/kernel/src/offset_backend.rs | 8 +- crates/kernel/src/ops.rs | 36 +- crates/kernel/src/outline_tests.rs | 4 +- crates/kernel/src/pinch_tests.rs | 4 +- crates/kernel/src/placement_tests.rs | 6 +- crates/kernel/src/planar_faces_tests.rs | 4 +- crates/kernel/src/planar_sources.rs | 18 +- crates/kernel/src/polybool.rs | 6 +- crates/kernel/src/polyhedron.rs | 10 +- crates/kernel/src/prism.rs | 28 +- crates/kernel/src/prism_local.rs | 4 +- crates/kernel/src/profile.rs | 4 +- crates/kernel/src/provenance.rs | 26 +- crates/kernel/src/record_tests.rs | 2 +- .../src/regularization_protection_tests.rs | 2 +- crates/kernel/src/sew.rs | 4 +- crates/kernel/src/smlib_sweeps.rs | 2 +- crates/kernel/src/smooth_edges.rs | 14 +- crates/kernel/src/step_in/conic_tests.rs | 2 +- crates/kernel/src/step_in/tests.rs | 28 +- crates/kernel/src/surface_offset.rs | 2 +- crates/kernel/src/surface_ruled.rs | 2 +- crates/kernel/src/surface_trim.rs | 4 +- crates/kernel/src/surfaces.rs | 4 +- crates/kernel/src/sweep_path.rs | 20 +- crates/kernel/src/tangent_cylinders.rs | 2 +- crates/kernel/src/taper_mesh_tests.rs | 2 +- crates/kernel/src/tess/tests.rs | 18 +- crates/kernel/src/tests.rs | 42 +- crates/kernel/src/thin_face_tests.rs | 4 +- crates/kernel/src/thread_display_tests.rs | 8 +- crates/kernel/src/thread_tests.rs | 8 +- crates/kernel/src/topo.rs | 6 +- crates/kernel/src/trimmed_face_mesh_tests.rs | 4 +- crates/kernel/src/tspline.rs | 6 +- crates/kernel/tests/chamfer_curved_holder.rs | 4 +- crates/kernel/tests/revolve_on_axis.rs | 2 +- crates/kernel/tests/shell_barrel.rs | 4 +- crates/ui-egui/src/dialogs_assembly.rs | 2 +- crates/ui-egui/src/preselect_tests.rs | 8 +- 130 files changed, 578 insertions(+), 879 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3770a76d1..01aa3c652b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4370,7 +4370,6 @@ dependencies = [ "solidcraft-step", "spade", "thiserror 2.0.21", - "truck-meshalgo", "truck-modeling", "truck-shapeops", "truck-stepio", diff --git a/Cargo.toml b/Cargo.toml index 6cb6b73087..b9aac01169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/apps/solidcraft-cli/src/kernel_cases.rs b/apps/solidcraft-cli/src/kernel_cases.rs index 5c3ac8b713..0b3c9ef366 100644 --- a/apps/solidcraft-cli/src/kernel_cases.rs +++ b/apps/solidcraft-cli/src/kernel_cases.rs @@ -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); diff --git a/apps/solidcraft-cli/src/oracle.rs b/apps/solidcraft-cli/src/oracle.rs index f7443c2e1e..ea5dda842d 100644 --- a/apps/solidcraft-cli/src/oracle.rs +++ b/apps/solidcraft-cli/src/oracle.rs @@ -187,7 +187,7 @@ fn step_export_check(s: &solidcraft_engine::Session, volume: f64) -> (bool, Stri fn our_mesh(s: &solidcraft_engine::Session) -> Result, String> { let mut ours: Vec = 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])) { diff --git a/apps/solidcraft-cli/tests/action_matrix.rs b/apps/solidcraft-cli/tests/action_matrix.rs index 21c34a8b62..977e2e72a7 100644 --- a/apps/solidcraft-cli/tests/action_matrix.rs +++ b/apps/solidcraft-cli/tests/action_matrix.rs @@ -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::(); + let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::(); 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::(); + let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::(); 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::(); + let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::(); 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::(); + let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::(); 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)) }); diff --git a/apps/solidcraft-cli/tests/bracket_example.rs b/apps/solidcraft-cli/tests/bracket_example.rs index 8056a87868..3a028e04d3 100644 --- a/apps/solidcraft-cli/tests/bracket_example.rs +++ b/apps/solidcraft-cli/tests/bracket_example.rs @@ -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); diff --git a/apps/solidcraft-cli/tests/design_corpus.rs b/apps/solidcraft-cli/tests/design_corpus.rs index 0e0bdc9d13..9eb0cfdcb1 100644 --- a/apps/solidcraft-cli/tests/design_corpus.rs +++ b/apps/solidcraft-cli/tests/design_corpus.rs @@ -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; diff --git a/apps/solidcraft-cli/tests/straight_edges_after_cuts.rs b/apps/solidcraft-cli/tests/straight_edges_after_cuts.rs index a52a24a181..89a0a7a2fb 100644 --- a/apps/solidcraft-cli/tests/straight_edges_after_cuts.rs +++ b/apps/solidcraft-cli/tests/straight_edges_after_cuts.rs @@ -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> = HashMap::new(); for (i, e) in mesh.edges.iter().enumerate() { diff --git a/crates/doc/src/display_quality.rs b/crates/doc/src/display_quality.rs index 041c3497b1..1572538911 100644 --- a/crates/doc/src/display_quality.rs +++ b/crates/doc/src/display_quality.rs @@ -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() }); } diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index f1110275d3..83fdf15ff7 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -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, 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 { diff --git a/crates/doc/src/eval_more.rs b/crates/doc/src/eval_more.rs index f7fb17000d..c2518319a4 100644 --- a/crates/doc/src/eval_more.rs +++ b/crates/doc/src/eval_more.rs @@ -198,7 +198,7 @@ pub(super) fn rib_tools(vals: &BTreeMap, st: &ModelState, f: &Fea fn planar_normal_at(b: &Body, p: Vec3) -> Option { 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 { 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)> = Vec::new(); @@ -2032,7 +2032,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap, 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| if l.signed_area() < 0.0 { l.iter().rev().copied().collect::>() } else { l.clone() }; diff --git a/crates/doc/src/joints_cam.rs b/crates/doc/src/joints_cam.rs index 54e57fba3e..4ef36b84e7 100644 --- a/crates/doc/src/joints_cam.rs +++ b/crates/doc/src/joints_cam.rs @@ -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 = - bodies.iter().map(|b| b.body.tessellate(tolerance(b.body.size()))).collect::>()?; + bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(tolerance(b.body.size())))).collect::>()?; let edges: Vec<(usize, &Vec, &[u32])> = meshes .iter() .enumerate() diff --git a/crates/doc/src/placed_mesh.rs b/crates/doc/src/placed_mesh.rs index 4a945b38a2..2ca1bd5d80 100644 --- a/crates/doc/src/placed_mesh.rs +++ b/crates/doc/src/placed_mesh.rs @@ -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> { - 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>, tolerance: f64) -> Arc { 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; diff --git a/crates/doc/src/sheet.rs b/crates/doc/src/sheet.rs index ed8cf03a3f..f75a08aa52 100644 --- a/crates/doc/src/sheet.rs +++ b/crates/doc/src/sheet.rs @@ -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) { diff --git a/crates/engine/src/boolean_regression_tests.rs b/crates/engine/src/boolean_regression_tests.rs index 9a6b73a4a9..8125c952ab 100644 --- a/crates/engine/src/boolean_regression_tests.rs +++ b/crates/engine/src/boolean_regression_tests.rs @@ -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²"); } diff --git a/crates/engine/src/cmd/clearance.rs b/crates/engine/src/cmd/clearance.rs index 902005f536..aec14f2422 100644 --- a/crates/engine/src/cmd/clearance.rs +++ b/crates/engine/src/cmd/clearance.rs @@ -489,7 +489,7 @@ fn clipped(body: &solidcraft_kernel::Body, region: Aabb3) -> Option Option { - 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) { diff --git a/crates/engine/src/cmd/compare.rs b/crates/engine/src/cmd/compare.rs index 439198c1c1..8e5da457da 100644 --- a/crates/engine/src/cmd/compare.rs +++ b/crates/engine/src/cmd/compare.rs @@ -161,7 +161,7 @@ fn compare_bodies(s: &mut Session, p: &Value) -> Result { // 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)> { diff --git a/crates/engine/src/cmd/component_clone.rs b/crates/engine/src/cmd/component_clone.rs index d244cc26f6..1d2e1e839a 100644 --- a/crates/engine/src/cmd/component_clone.rs +++ b/crates/engine/src/cmd/component_clone.rs @@ -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)); diff --git a/crates/engine/src/cmd/component_clone_tests.rs b/crates/engine/src/cmd/component_clone_tests.rs index b2888fdd64..35078a3215 100644 --- a/crates/engine/src/cmd/component_clone_tests.rs +++ b/crates/engine/src/cmd/component_clone_tests.rs @@ -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)] { diff --git a/crates/engine/src/cmd/config_certificate_tests.rs b/crates/engine/src/cmd/config_certificate_tests.rs index 34fd0d8f09..61b9c81e4e 100644 --- a/crates/engine/src/cmd/config_certificate_tests.rs +++ b/crates/engine/src/cmd/config_certificate_tests.rs @@ -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::>())); 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] diff --git a/crates/engine/src/cmd/contact_sweep.rs b/crates/engine/src/cmd/contact_sweep.rs index e9a3593a21..d585349189 100644 --- a/crates/engine/src/cmd/contact_sweep.rs +++ b/crates/engine/src/cmd/contact_sweep.rs @@ -52,7 +52,7 @@ pub(crate) struct ContactMeshes(HashMap); /// Tessellate `body` for contact checks and measure how thin it is. fn contact_mesh(body: &solidcraft_doc::ModelBody) -> Result { 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")); } diff --git a/crates/engine/src/cmd/edit_tests.rs b/crates/engine/src/cmd/edit_tests.rs index 01d8317eb3..c54ef1f2e3 100644 --- a/crates/engine/src/cmd/edit_tests.rs +++ b/crates/engine/src/cmd/edit_tests.rs @@ -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); diff --git a/crates/engine/src/cmd/features_more_tests.rs b/crates/engine/src/cmd/features_more_tests.rs index a3fb1bc8a8..e80db0c5f5 100644 --- a/crates/engine/src/cmd/features_more_tests.rs +++ b/crates/engine/src/cmd/features_more_tests.rs @@ -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 = (0..positions.len()).collect(); let root = |piece: &mut Vec, 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. diff --git a/crates/engine/src/cmd/lineage_face_tests.rs b/crates/engine/src/cmd/lineage_face_tests.rs index 680dfb1fdd..0efd8c9a2b 100644 --- a/crates/engine/src/cmd/lineage_face_tests.rs +++ b/crates/engine/src/cmd/lineage_face_tests.rs @@ -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| { diff --git a/crates/engine/src/cmd/motion.rs b/crates/engine/src/cmd/motion.rs index 4bef2b70cf..cebfb09f0e 100644 --- a/crates/engine/src/cmd/motion.rs +++ b/crates/engine/src/cmd/motion.rs @@ -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 { - 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()); } } diff --git a/crates/engine/src/cmd/revolve_convention_tests.rs b/crates/engine/src/cmd/revolve_convention_tests.rs index 4e1d55de48..ed9f5ac5cb 100644 --- a/crates/engine/src/cmd/revolve_convention_tests.rs +++ b/crates/engine/src/cmd/revolve_convention_tests.rs @@ -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))); diff --git a/crates/engine/src/cmd/sheet_tests.rs b/crates/engine/src/cmd/sheet_tests.rs index b7c2ce3f76..78fcbe1366 100644 --- a/crates/engine/src/cmd/sheet_tests.rs +++ b/crates/engine/src/cmd/sheet_tests.rs @@ -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(); diff --git a/crates/engine/src/cmd/sketch_face.rs b/crates/engine/src/cmd/sketch_face.rs index d8fa720a4b..3dff68c905 100644 --- a/crates/engine/src/cmd/sketch_face.rs +++ b/crates/engine/src/cmd/sketch_face.rs @@ -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). diff --git a/crates/engine/src/cmd/sketch_face_tests.rs b/crates/engine/src/cmd/sketch_face_tests.rs index d7d7201741..323711de76 100644 --- a/crates/engine/src/cmd/sketch_face_tests.rs +++ b/crates/engine/src/cmd/sketch_face_tests.rs @@ -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.))); diff --git a/crates/engine/src/cmd/sketch_project_tests.rs b/crates/engine/src/cmd/sketch_project_tests.rs index 066c5bc41e..3c2fe079d7 100644 --- a/crates/engine/src/cmd/sketch_project_tests.rs +++ b/crates/engine/src/cmd/sketch_project_tests.rs @@ -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()); diff --git a/crates/engine/src/cmd/surface.rs b/crates/engine/src/cmd/surface.rs index 24457190b9..8d0a91eb20 100644 --- a/crates/engine/src/cmd/surface.rs +++ b/crates/engine/src/cmd/surface.rs @@ -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}"); diff --git a/crates/engine/src/cmd/surface_boundary_tests.rs b/crates/engine/src/cmd/surface_boundary_tests.rs index fd9f0f81ab..e48205eb3d 100644 --- a/crates/engine/src/cmd/surface_boundary_tests.rs +++ b/crates/engine/src/cmd/surface_boundary_tests.rs @@ -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"); diff --git a/crates/engine/src/cmd/sweep_options_tests.rs b/crates/engine/src/cmd/sweep_options_tests.rs index 2d6b804bf1..f85e7c8544 100644 --- a/crates/engine/src/cmd/sweep_options_tests.rs +++ b/crates/engine/src/cmd/sweep_options_tests.rs @@ -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)); diff --git a/crates/engine/src/cmd/view_dxf_meshes.rs b/crates/engine/src/cmd/view_dxf_meshes.rs index 48cbfe6a81..0c6a40496b 100644 --- a/crates/engine/src/cmd/view_dxf_meshes.rs +++ b/crates/engine/src/cmd/view_dxf_meshes.rs @@ -127,7 +127,7 @@ pub(super) fn prepare(s: &Session, tolerance: f64) -> Result { 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))); } diff --git a/crates/engine/src/display_mesh_tests.rs b/crates/engine/src/display_mesh_tests.rs index 25d717b728..10625ca5b2 100644 --- a/crates/engine/src/display_mesh_tests.rs +++ b/crates/engine/src/display_mesh_tests.rs @@ -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:?}"); diff --git a/crates/engine/src/pattern_suppress_tests.rs b/crates/engine/src/pattern_suppress_tests.rs index 89182fc82b..4c9b88c32b 100644 --- a/crates/engine/src/pattern_suppress_tests.rs +++ b/crates/engine/src/pattern_suppress_tests.rs @@ -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), diff --git a/crates/engine/src/profile_curves_tests.rs b/crates/engine/src/profile_curves_tests.rs index f0168807d7..77aa1e3eb0 100644 --- a/crates/engine/src/profile_curves_tests.rs +++ b/crates/engine/src/profile_curves_tests.rs @@ -14,7 +14,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value { } fn volumes(s: &Session) -> Vec { - 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 { diff --git a/crates/engine/src/profile_fallback_tests.rs b/crates/engine/src/profile_fallback_tests.rs index 34d84db890..38642a1dfa 100644 --- a/crates/engine/src/profile_fallback_tests.rs +++ b/crates/engine/src/profile_fallback_tests.rs @@ -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, Option) { diff --git a/crates/engine/src/profile_refs_tests.rs b/crates/engine/src/profile_refs_tests.rs index dd38479e66..e0fe9f65ab 100644 --- a/crates/engine/src/profile_refs_tests.rs +++ b/crates/engine/src/profile_refs_tests.rs @@ -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] diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index dea0aaeba8..565d6a6674 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -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() diff --git a/crates/engine/src/tests/boolean_diagnostics.rs b/crates/engine/src/tests/boolean_diagnostics.rs index 3d64352202..4f52bae4e9 100644 --- a/crates/engine/src/tests/boolean_diagnostics.rs +++ b/crates/engine/src/tests/boolean_diagnostics.rs @@ -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}"); } diff --git a/crates/engine/src/tests/curved_seam_round.rs b/crates/engine/src/tests/curved_seam_round.rs index 5eda745684..971ea89af3 100644 --- a/crates/engine/src/tests/curved_seam_round.rs +++ b/crates/engine/src/tests/curved_seam_round.rs @@ -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 = (0..720) .map(|i| { let a = 2.0 * PI * i as f64 / 720.0; diff --git a/crates/engine/src/tests/flush_join_loft.rs b/crates/engine/src/tests/flush_join_loft.rs index 18598e5da3..eb38e00cfa 100644 --- a/crates/engine/src/tests/flush_join_loft.rs +++ b/crates/engine/src/tests/flush_join_loft.rs @@ -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"); } diff --git a/crates/engine/src/tests/loft_ring.rs b/crates/engine/src/tests/loft_ring.rs index 8438829dac..7263ffae27 100644 --- a/crates/engine/src/tests/loft_ring.rs +++ b/crates/engine/src/tests/loft_ring.rs @@ -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!({})); diff --git a/crates/engine/src/tests/mesh_convert.rs b/crates/engine/src/tests/mesh_convert.rs index 33a4bb8414..a4c18a06f7 100644 --- a/crates/engine/src/tests/mesh_convert.rs +++ b/crates/engine/src/tests/mesh_convert.rs @@ -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 { diff --git a/crates/engine/src/tests/robustness.rs b/crates/engine/src/tests/robustness.rs index 7e71d95cb4..df49cb5a21 100644 --- a/crates/engine/src/tests/robustness.rs +++ b/crates/engine/src/tests/robustness.rs @@ -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); } diff --git a/crates/engine/src/thread_tests.rs b/crates/engine/src/thread_tests.rs index e0424290ae..b97295f83b 100644 --- a/crates/engine/src/thread_tests.rs +++ b/crates/engine/src/thread_tests.rs @@ -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); } diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index 9492fe9088..bb067ddf0d 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -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 = bodies().iter().map(|(_, b)| ok(b.display_plan_angular(0.01, std::f64::consts::PI / 9.0))).collect(); + let plans: Vec = 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(); diff --git a/crates/io/src/lib.rs b/crates/io/src/lib.rs index a4abc38a43..22d8d479a9 100644 --- a/crates/io/src/lib.rs +++ b/crates/io/src/lib.rs @@ -391,7 +391,7 @@ pub fn export_mesh(b: &solidcraft_kernel::Body) -> Result { /// [`export_mesh`] at `quality` (never finer than 1 µm). pub fn export_mesh_at(b: &solidcraft_kernel::Body, quality: MeshQuality) -> Result { - 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). diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 4eee0553ec..b41ac8f1b6 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -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"] } diff --git a/crates/kernel/src/blend.rs b/crates/kernel/src/blend.rs index 6da010d675..3d7fb397a1 100644 --- a/crates/kernel/src/blend.rs +++ b/crates/kernel/src/blend.rs @@ -2520,7 +2520,7 @@ fn chamfer_tool_curved(cur: &Body, pick: Vec3, distance: f64) -> Result // 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(); diff --git a/crates/kernel/src/blend_corner.rs b/crates/kernel/src/blend_corner.rs index 54effa4780..750b46a810 100644 --- a/crates/kernel/src/blend_corner.rs +++ b/crates/kernel/src/blend_corner.rs @@ -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 diff --git a/crates/kernel/src/blend_fit.rs b/crates/kernel/src/blend_fit.rs index 3cb6a4a5d4..b446b2f8dc 100644 --- a/crates/kernel/src/blend_fit.rs +++ b/crates/kernel/src/blend_fit.rs @@ -271,7 +271,7 @@ fn select(body: &Body, pts: &[Vec3]) -> Result { fn select_in(body: &Body, pts: &[Vec3]) -> Result { 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 = 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 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 diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index 8d4614adf5..94c8cc30dc 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -88,7 +88,7 @@ pub(crate) struct Tolerances { #[derive(Debug)] pub(crate) struct MeshMemo { solid: std::sync::Arc, - made: std::sync::Mutex)>>, + made: Remembered, /// The meshes a boolean makes of the solid's shells, by tolerance. shells: std::sync::Mutex>)>>, } @@ -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)>>; + +/// 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) -> Result> { + 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>, + 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, } -type MeshedShell = truck_topology::Shell, Option>; /// 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 { - 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, } -/// [`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 { - 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> { - 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> { - 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) { + 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>::new(); - let mut at = |a: &V, b: &V| -> Option { - 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 { - 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 { - 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::(); - 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 { - 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 { + 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 { - 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 { + 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 { - self.tessellate_within(tol, f64::INFINITY, refine) + fn mesh_untimed(&self, t: &MeshTolerance) -> Result { + 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 { - 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 { + crate::profile::timed(crate::profile::Kind::Tessellate, || self.mesh_plan_untimed(t)) + } + + fn mesh_plan_untimed(&self, t: &MeshTolerance) -> Result { + 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 { + 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 { - 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 { - 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 { - self.plan_within(tol, Some(angle)) - } - - fn plan_within(&self, tol: f64, angle: Option) -> Result { - crate::profile::timed(crate::profile::Kind::Tessellate, || self.plan_within_untimed(tol, angle)) - } - - fn plan_within_untimed(&self, tol: f64, angle: Option) -> Result { - 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 { - crate::profile::timed(crate::profile::Kind::Tessellate, || self.display_mesh_untimed(tol)) - } - - fn display_mesh_untimed(&self, tol: f64) -> Result { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { 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> { - 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> { - 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) } diff --git a/crates/kernel/src/body_lump_tests.rs b/crates/kernel/src/body_lump_tests.rs index 89aa73ada7..3a5cf9fded 100644 --- a/crates/kernel/src/body_lump_tests.rs +++ b/crates/kernel/src/body_lump_tests.rs @@ -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}"); } diff --git a/crates/kernel/src/boolean_guard.rs b/crates/kernel/src/boolean_guard.rs index aafcadd423..43c2ed3966 100644 --- a/crates/kernel/src/boolean_guard.rs +++ b/crates/kernel/src/boolean_guard.rs @@ -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)?); diff --git a/crates/kernel/src/boolean_history_tests.rs b/crates/kernel/src/boolean_history_tests.rs index f090cec832..9a939f8858 100644 --- a/crates/kernel/src/boolean_history_tests.rs +++ b/crates/kernel/src/boolean_history_tests.rs @@ -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 = operands.iter().map(|b| b.tessellate(tol).unwrap()).collect(); + let rm = r.mesh(&crate::MeshTolerance::chord(tol)).unwrap(); + let ms: Vec = 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(); diff --git a/crates/kernel/src/bore_union_tests.rs b/crates/kernel/src/bore_union_tests.rs index 7dd91a1ee3..60542ddff9 100644 --- a/crates/kernel/src/bore_union_tests.rs +++ b/crates/kernel/src/bore_union_tests.rs @@ -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}"); } diff --git a/crates/kernel/src/boundary_network.rs b/crates/kernel/src/boundary_network.rs index ea109e4ad7..efb35411ec 100644 --- a/crates/kernel/src/boundary_network.rs +++ b/crates/kernel/src/boundary_network.rs @@ -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 }; diff --git a/crates/kernel/src/candidate_reach_tests.rs b/crates/kernel/src/candidate_reach_tests.rs index e237b6b6de..c2276d7535 100644 --- a/crates/kernel/src/candidate_reach_tests.rs +++ b/crates/kernel/src/candidate_reach_tests.rs @@ -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); } } diff --git a/crates/kernel/src/clearance.rs b/crates/kernel/src/clearance.rs index 3a505e3aaa..aa33cf60fa 100644 --- a/crates/kernel/src/clearance.rs +++ b/crates/kernel/src/clearance.rs @@ -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> { - 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 }) } diff --git a/crates/kernel/src/coaxial_overlap.rs b/crates/kernel/src/coaxial_overlap.rs index c7331f18b5..d619ee1d2f 100644 --- a/crates/kernel/src/coaxial_overlap.rs +++ b/crates/kernel/src/coaxial_overlap.rs @@ -341,7 +341,7 @@ fn flush(x: &Body, y: &Body, same: f64, depth: f64) -> Vec { 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 { /// 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, same: f64) -> Option> { let mut out: Vec = 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. diff --git a/crates/kernel/src/coplanar.rs b/crates/kernel/src/coplanar.rs index 9f40592106..863822f616 100644 --- a/crates/kernel/src/coplanar.rs +++ b/crates/kernel/src/coplanar.rs @@ -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 = 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 = 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)); diff --git a/crates/kernel/src/coplanar_tests.rs b/crates/kernel/src/coplanar_tests.rs index 7700bb5aeb..f794eb61e8 100644 --- a/crates/kernel/src/coplanar_tests.rs +++ b/crates/kernel/src/coplanar_tests.rs @@ -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}"); } diff --git a/crates/kernel/src/curveblend_law_tests.rs b/crates/kernel/src/curveblend_law_tests.rs index f9d5195d9f..44495a1475 100644 --- a/crates/kernel/src/curveblend_law_tests.rs +++ b/crates/kernel/src/curveblend_law_tests.rs @@ -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() diff --git a/crates/kernel/src/curveblend_tests.rs b/crates/kernel/src/curveblend_tests.rs index ae70fc7c34..9e7a63f3b2 100644 --- a/crates/kernel/src/curveblend_tests.rs +++ b/crates/kernel/src/curveblend_tests.rs @@ -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::() * 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; diff --git a/crates/kernel/src/curvecorner_tests.rs b/crates/kernel/src/curvecorner_tests.rs index 3153d107ce..947b200324 100644 --- a/crates/kernel/src/curvecorner_tests.rs +++ b/crates/kernel/src/curvecorner_tests.rs @@ -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]| { - 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 { diff --git a/crates/kernel/src/delete_face.rs b/crates/kernel/src/delete_face.rs index e7cc07de6f..23b3608b0c 100644 --- a/crates/kernel/src/delete_face.rs +++ b/crates/kernel/src/delete_face.rs @@ -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 { - 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| { diff --git a/crates/kernel/src/display_mesh_tests.rs b/crates/kernel/src/display_mesh_tests.rs index d94945e20a..73e3c77a7c 100644 --- a/crates/kernel/src/display_mesh_tests.rs +++ b/crates/kernel/src/display_mesh_tests.rs @@ -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 = (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::(); 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), diff --git a/crates/kernel/src/emboss.rs b/crates/kernel/src/emboss.rs index 94acc8c172..ec8563ef90 100644 --- a/crates/kernel/src/emboss.rs +++ b/crates/kernel/src/emboss.rs @@ -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))); } } } diff --git a/crates/kernel/src/face_source_tests.rs b/crates/kernel/src/face_source_tests.rs index 59a1dd834f..6b52e8033b 100644 --- a/crates/kernel/src/face_source_tests.rs +++ b/crates/kernel/src/face_source_tests.rs @@ -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)); diff --git a/crates/kernel/src/fill.rs b/crates/kernel/src/fill.rs index ce2b8e05c0..e9adfe9845 100644 --- a/crates/kernel/src/fill.rs +++ b/crates/kernel/src/fill.rs @@ -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(); diff --git a/crates/kernel/src/fill_general_tests.rs b/crates/kernel/src/fill_general_tests.rs index 3ac620ca32..cc1f0f2887 100644 --- a/crates/kernel/src/fill_general_tests.rs +++ b/crates/kernel/src/fill_general_tests.rs @@ -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(); diff --git a/crates/kernel/src/helix.rs b/crates/kernel/src/helix.rs index 482b0294c5..f94b54c523 100644 --- a/crates/kernel/src/helix.rs +++ b/crates/kernel/src/helix.rs @@ -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); } diff --git a/crates/kernel/src/history_tests.rs b/crates/kernel/src/history_tests.rs index d6e57935b4..34fd94c834 100644 --- a/crates/kernel/src/history_tests.rs +++ b/crates/kernel/src/history_tests.rs @@ -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> = 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::>()); } diff --git a/crates/kernel/src/iges_in/tests.rs b/crates/kernel/src/iges_in/tests.rs index b253f3b19f..4abed67300 100644 --- a/crates/kernel/src/iges_in/tests.rs +++ b/crates/kernel/src/iges_in/tests.rs @@ -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() { diff --git a/crates/kernel/src/issue_616_tests.rs b/crates/kernel/src/issue_616_tests.rs index 5d6d9e0b88..6cfa8d381d 100644 --- a/crates/kernel/src/issue_616_tests.rs +++ b/crates/kernel/src/issue_616_tests.rs @@ -54,7 +54,7 @@ fn moved_outside(from: &Mesh, onto: &Mesh, region: &Aabb3, tol: f64) -> Option 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 { diff --git a/crates/kernel/src/join_between_caps_tests.rs b/crates/kernel/src/join_between_caps_tests.rs index a5c171fc6d..f4c76fb686 100644 --- a/crates/kernel/src/join_between_caps_tests.rs +++ b/crates/kernel/src/join_between_caps_tests.rs @@ -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:?}"); diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index c701d33fcd..4056b546e4 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -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}; diff --git a/crates/kernel/src/lip.rs b/crates/kernel/src/lip.rs index 7faed0fb38..ec7c51b731 100644 --- a/crates/kernel/src/lip.rs +++ b/crates/kernel/src/lip.rs @@ -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() diff --git a/crates/kernel/src/loft_region_tests.rs b/crates/kernel/src/loft_region_tests.rs index 462ea8217a..e20400c4ee 100644 --- a/crates/kernel/src/loft_region_tests.rs +++ b/crates/kernel/src/loft_region_tests.rs @@ -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))); diff --git a/crates/kernel/src/measure.rs b/crates/kernel/src/measure.rs index 3cae7e9932..6e7feda36f 100644 --- a/crates/kernel/src/measure.rs +++ b/crates/kernel/src/measure.rs @@ -89,7 +89,7 @@ pub struct MassMeasure { pub fn measure(b: &Body) -> Result { 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 { 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 { fn mass(b: &Body, topology: bool) -> Result<(MassMeasure, Option)> { 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)> { #[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()); diff --git a/crates/kernel/src/mesh_convert.rs b/crates/kernel/src/mesh_convert.rs index ebe19af921..6a062f97a2 100644 --- a/crates/kernel/src/mesh_convert.rs +++ b/crates/kernel/src/mesh_convert.rs @@ -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)); diff --git a/crates/kernel/src/mesh_memo_tests.rs b/crates/kernel/src/mesh_memo_tests.rs index 915b1dcaf1..f5807631e2 100644 --- a/crates/kernel/src/mesh_memo_tests.rs +++ b/crates/kernel/src/mesh_memo_tests.rs @@ -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()); } diff --git a/crates/kernel/src/modify_history_tests.rs b/crates/kernel/src/modify_history_tests.rs index ccbe6bff5c..4dc845d13a 100644 --- a/crates/kernel/src/modify_history_tests.rs +++ b/crates/kernel/src/modify_history_tests.rs @@ -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)); diff --git a/crates/kernel/src/native/adapter_tests.rs b/crates/kernel/src/native/adapter_tests.rs index 524172902d..64379ffd41 100644 --- a/crates/kernel/src/native/adapter_tests.rs +++ b/crates/kernel/src/native/adapter_tests.rs @@ -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 = body.solid.boundaries().iter().flat_map(|sh| sh.face_iter().map(|f| f.orientation()).collect::>()).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}"); diff --git a/crates/kernel/src/offset_backend.rs b/crates/kernel/src/offset_backend.rs index cc3fec73e8..784de47e4d 100644 --- a/crates/kernel/src/offset_backend.rs +++ b/crates/kernel/src/offset_backend.rs @@ -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> { 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 = Vec::new(); for p in at { let near = mesh @@ -179,7 +179,7 @@ fn faces_under(b: &Body, at: &[Vec3]) -> Result> { /// One point inside each face `faces` of `b`: the centroid of its largest triangle. fn face_points(b: &Body, faces: &[usize]) -> Result> { - 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 { 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() }; diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs index 34abd4d116..12d7f12c9c 100644 --- a/crates/kernel/src/ops.rs +++ b/crates/kernel/src/ops.rs @@ -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, lb: Vec, op: BoolOp) -> Result> { 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> { // 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); } diff --git a/crates/kernel/src/outline_tests.rs b/crates/kernel/src/outline_tests.rs index 7cd95417df..58202ac331 100644 --- a/crates/kernel/src/outline_tests.rs +++ b/crates/kernel/src/outline_tests.rs @@ -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() diff --git a/crates/kernel/src/pinch_tests.rs b/crates/kernel/src/pinch_tests.rs index 7fd7331e95..7a567dbb3c 100644 --- a/crates/kernel/src/pinch_tests.rs +++ b/crates/kernel/src/pinch_tests.rs @@ -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); diff --git a/crates/kernel/src/placement_tests.rs b/crates/kernel/src/placement_tests.rs index 2a038bf08c..5fd4e7e07f 100644 --- a/crates/kernel/src/placement_tests.rs +++ b/crates/kernel/src/placement_tests.rs @@ -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.]]); diff --git a/crates/kernel/src/planar_faces_tests.rs b/crates/kernel/src/planar_faces_tests.rs index 1c6788f465..82942afb26 100644 --- a/crates/kernel/src/planar_faces_tests.rs +++ b/crates/kernel/src/planar_faces_tests.rs @@ -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}"); } diff --git a/crates/kernel/src/planar_sources.rs b/crates/kernel/src/planar_sources.rs index 8637b27260..5e75700d72 100644 --- a/crates/kernel/src/planar_sources.rs +++ b/crates/kernel/src/planar_sources.rs @@ -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.))); diff --git a/crates/kernel/src/polybool.rs b/crates/kernel/src/polybool.rs index ca72b59309..8c080b8212 100644 --- a/crates/kernel/src/polybool.rs +++ b/crates/kernel/src/polybool.rs @@ -287,7 +287,7 @@ fn planar_polys(b: &Body, operand: usize) -> std::result::Result, 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, NotP /// pieces of its faces as operand `operand`. fn faceted_polys(b: &Body, tol: f64, operand: usize) -> Option> { let faces = b.faces(tol).ok()?; - let mesh = b.tessellate(tol).ok()?; + let mesh = b.mesh(&crate::MeshTolerance::chord(tol)).ok()?; let planes: Vec> = 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 Result { 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 = Vec::new(); for p in at { let near = mesh @@ -470,7 +470,7 @@ fn shell_planar(b: &Body, open: &[Vec3], thickness: f64) -> Result { 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 = 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 = 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 = 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> { 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 = Vec::new(); for p in at { let near = mesh diff --git a/crates/kernel/src/prism.rs b/crates/kernel/src/prism.rs index 68590a4f4d..d1be421f35 100644 --- a/crates/kernel/src/prism.rs +++ b/crates/kernel/src/prism.rs @@ -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), diff --git a/crates/kernel/src/prism_local.rs b/crates/kernel/src/prism_local.rs index 8456d45377..e2bbbcfc2f 100644 --- a/crates/kernel/src/prism_local.rs +++ b/crates/kernel/src/prism_local.rs @@ -65,7 +65,7 @@ pub(crate) fn local_prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option Opt } }) .collect(); - let mesh = body.tessellate(mesh_tol).ok()?; + let mesh = body.mesh(&crate::MeshTolerance::chord(mesh_tol)).ok()?; let here = widened(extent(&mesh.positions, axes)?, mesh_tol); let mut pieces = Vec::new(); if off.iter().any(|x| *x) { diff --git a/crates/kernel/src/profile.rs b/crates/kernel/src/profile.rs index bd40bd4994..2ea09d4c55 100644 --- a/crates/kernel/src/profile.rs +++ b/crates/kernel/src/profile.rs @@ -166,12 +166,12 @@ mod tests { use crate::{BoolOp, box_solid, faceted_boolean}; use solidcraft_geom::Vec3; let a = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap(); - let tool = box_solid(Vec3::new(5.0, 5.0, 5.0), Vec3::new(15.0, 15.0, 15.0)).unwrap().tessellate(0.1).unwrap(); + let tool = box_solid(Vec3::new(5.0, 5.0, 5.0), Vec3::new(15.0, 15.0, 15.0)).unwrap().mesh(&crate::MeshTolerance::chord(0.1)).unwrap(); // The thread groove's route: the faceting it does first is the boolean's time too. let (r, t) = capture(|| faceted_boolean(&a, &tool, BoolOp::Cut, 0.01)); assert!(r.unwrap().is_some()); assert!(t.boolean_ms > 0.0 && t.tessellate_ms == 0.0, "{t:?}"); - let (m, t) = capture(|| a.reference_display_mesh_angular(0.01, crate::DISPLAY_ANGLE)); + let (m, t) = capture(|| a.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(crate::DISPLAY_ANGLE) })); assert!(!m.unwrap().triangles.is_empty()); assert!(t.tessellate_ms > 0.0 && t.boolean_ms == 0.0, "{t:?}"); } diff --git a/crates/kernel/src/provenance.rs b/crates/kernel/src/provenance.rs index a02c0cf458..89b7c61546 100644 --- a/crates/kernel/src/provenance.rs +++ b/crates/kernel/src/provenance.rs @@ -96,11 +96,11 @@ pub fn trace(result: &Body, inputs: &[&Body]) -> Result { let size = inputs.iter().map(|b| b.size()).fold(result.size(), f64::max).max(1e-9); let mesh_tol = size * 2e-4; let tol = mesh_tol * 2.0; - let rm = result.tessellate(mesh_tol)?; + let rm = result.mesh(&crate::MeshTolerance::chord(mesh_tol))?; let rfaces = face_tris(&rm); let mut imeshes = Vec::with_capacity(inputs.len()); for b in inputs { - imeshes.push(b.tessellate(mesh_tol)?); + imeshes.push(b.mesh(&crate::MeshTolerance::chord(mesh_tol))?); } let ifaces: Vec> = imeshes.iter().map(face_tris).collect(); // Each result face: the input faces most of its samples lie on. @@ -186,7 +186,7 @@ fn nearest_segment(segs: &[Seg2], pts: &[Vec2]) -> Option { /// caps on the start and end planes, sides by the profile segment they sweep. pub fn extrude_origins(tool: &Body, plane: &Plane, regions: &[Region2]) -> Result> { let size = tool.size().max(1e-9); - let m = tool.tessellate(size * 2e-4)?; + let m = tool.mesh(&crate::MeshTolerance::chord(size * 2e-4))?; let faces = face_tris(&m); let n = plane.normal(); let segs = segments(regions); @@ -216,7 +216,7 @@ pub fn extrude_origins(tool: &Body, plane: &Plane, regions: &[Region2]) -> Resul /// by the profile segment it turns. pub fn revolve_origins(tool: &Body, plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2) -> Result> { let size = tool.size().max(1e-9); - let m = tool.tessellate(size * 2e-4)?; + let m = tool.mesh(&crate::MeshTolerance::chord(size * 2e-4))?; let faces = face_tris(&m); let segs = segments(regions); let o = plane.to_world(axis_origin); @@ -261,8 +261,8 @@ pub fn revolve_origins(tool: &Body, plane: &Plane, regions: &[Region2], axis_ori /// edge indices in `input`'s mesh order): each blend face comes from the edge it lies along. pub fn blend_origins(result: &Body, input: &Body, edges: &[usize], prov: &mut History) -> Result<()> { let size = input.size().max(1e-9); - let rm = result.tessellate(size * 2e-4)?; - let im = input.tessellate(size * 2e-4)?; + let rm = result.mesh(&crate::MeshTolerance::chord(size * 2e-4))?; + let im = input.mesh(&crate::MeshTolerance::chord(size * 2e-4))?; let rfaces = face_tris(&rm); for (fi, fp) in prov.faces.iter_mut().enumerate() { if fp.change != Change::Generated { @@ -288,8 +288,8 @@ pub fn blend_origins(result: &Body, input: &Body, edges: &[usize], prov: &mut Hi pub fn offset_origins(result: &Body, input: &Body, thickness: f64, prov: &mut History) -> Result<()> { let size = input.size().max(1e-9); let tol = size * 4e-4; - let rm = result.tessellate(size * 2e-4)?; - let im = input.tessellate(size * 2e-4)?; + let rm = result.mesh(&crate::MeshTolerance::chord(size * 2e-4))?; + let im = input.mesh(&crate::MeshTolerance::chord(size * 2e-4))?; let rfaces = face_tris(&rm); let ifaces = face_tris(&im); for (fi, fp) in prov.faces.iter_mut().enumerate() { @@ -312,7 +312,7 @@ pub fn offset_origins(result: &Body, input: &Body, thickness: f64, prov: &mut Hi /// that plane sweeps the profile segment under the edge, and the rest are end caps. pub fn swept_origins(tool: &Body, plane: &Plane, regions: &[Region2]) -> Result> { let size = tool.size().max(1e-9); - let m = tool.tessellate(size * 2e-4)?; + let m = tool.mesh(&crate::MeshTolerance::chord(size * 2e-4))?; let faces = face_tris(&m); let segs = segments(regions); let flat = |p: Vec3| plane.height(p).abs() <= size * 1e-6; @@ -347,7 +347,7 @@ pub fn sweep_origins(tool: &Body, plane: &Plane, regions: &[Region2]) -> Result< let k = segments(regions).len(); let size = tool.size().max(1e-9); if k > 0 && n >= 2 && (n - 2).is_multiple_of(k) { - let m = tool.tessellate(size * 2e-4)?; + let m = tool.mesh(&crate::MeshTolerance::chord(size * 2e-4))?; let faces = face_tris(&m); let start_flat = faces.get(n - 2).is_some_and(|f| f.tris.iter().all(|(t, _)| t.iter().all(|p| plane.height(*p).abs() <= size * 1e-6))); if start_flat { @@ -391,7 +391,7 @@ mod tests { use crate::{BoolOp, boolean, box_solid, cylinder}; fn face_at(b: &crate::Body, p: Vec3) -> usize { - let m = b.tessellate(1e-3).unwrap(); + let m = b.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap(); let mut best = (f64::INFINITY, 0); for (t, f) in m.triangles.iter().zip(&m.tri_face) { let q = m.tri(t).unwrap(); @@ -437,7 +437,7 @@ mod tests { assert_eq!(p.faces[top].origins.len(), 2); } // Edges: the plate's untouched vertical edges come from the plate's edges. - let m = cut.tessellate(1e-3).unwrap(); + let m = cut.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap(); let p = trace(&cut, &[&a, &pin]).unwrap(); let corner = (0..m.edges.len()).find(|i| m.edges[*i].iter().all(|q| q.x.abs() < 1e-9 && q.y.abs() < 1e-9)).unwrap(); assert!(matches!(p.edges[corner].origins[..], [super::EdgeOrigin::Edge { operand: 0, .. }])); @@ -479,7 +479,7 @@ mod tests { assert_eq!(ro[face_at(&rt, Vec3::new(15.0, 15.0, 0.0))], FaceOrigin::Cap { end: false }); // A fillet: the blend face comes from the edge it replaced. let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)).unwrap(); - let bm = b.tessellate(1e-3).unwrap(); + let bm = b.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap(); let e = (0..bm.edges.len()).find(|i| bm.edges[*i].iter().all(|q| (q.y - 30.0).abs() < 1e-9 && (q.z - 20.0).abs() < 1e-9)).unwrap(); let f = crate::fillet(&b, &[Vec3::new(20.0, 30.0, 20.0)], 5.0).unwrap(); let mut p: History = trace(&f, &[&b]).unwrap(); diff --git a/crates/kernel/src/record_tests.rs b/crates/kernel/src/record_tests.rs index b03325d3b7..c6e7372118 100644 --- a/crates/kernel/src/record_tests.rs +++ b/crates/kernel/src/record_tests.rs @@ -38,7 +38,7 @@ fn points(b: &Body) -> Vec> { /// Mesh vertices of each face, and its triangles' centres (off a curved face by up to the /// mesh's tolerance). fn mesh_points(b: &Body) -> (Vec>, Vec>) { - let m = b.tessellate(b.size() * 1e-3).unwrap(); + let m = b.mesh(&crate::MeshTolerance::chord(b.size() * 1e-3)).unwrap(); let (mut corners, mut centres) = (vec![Vec::new(); b.face_count()], vec![Vec::new(); b.face_count()]); for (t, f) in m.triangles.iter().zip(&m.tri_face) { let c = t.map(|i| m.positions[i as usize]); diff --git a/crates/kernel/src/regularization_protection_tests.rs b/crates/kernel/src/regularization_protection_tests.rs index 5bcab8ab8e..55e833243e 100644 --- a/crates/kernel/src/regularization_protection_tests.rs +++ b/crates/kernel/src/regularization_protection_tests.rs @@ -30,7 +30,7 @@ fn stamped_shell_keeps_authored_painted_and_split_boundaries() { assert!(result.face_count() > ordinary.face_count(), "authored boundaries must survive both Shell regularizers"); assert!(result.validity().is_empty()); let expected = 40. * 30. * 24. - 38. * 28. * 23.; - let mesh = result.tessellate(1e-5).unwrap(); + let mesh = result.mesh(&crate::MeshTolerance::chord(1e-5)).unwrap(); assert!((mesh.measure().volume - expected).abs() < 1e-6); assert!(mesh.contains(Vec3::new(19.5, 0., 12.))); assert!(!mesh.contains(Vec3::new(0., 0., 12.))); diff --git a/crates/kernel/src/sew.rs b/crates/kernel/src/sew.rs index ce9123dcb5..36e46ba6f2 100644 --- a/crates/kernel/src/sew.rs +++ b/crates/kernel/src/sew.rs @@ -1005,7 +1005,7 @@ mod tests { let lumps = body.lumps().unwrap(); assert_eq!(lumps.len(), 2); assert!(lumps.iter().all(|b| (crate::measure(b).unwrap().volume - 24.0).abs() < 1e-9)); - let mesh = body.tessellate(0.01).unwrap(); + let mesh = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(); assert!(mesh.contains(Vec3::new(1.0, 1.0, 1.0))); assert!(mesh.contains(Vec3::new(11.0, 1.0, 1.0))); assert!(!mesh.contains(Vec3::new(5.0, 1.0, 1.0))); @@ -1034,7 +1034,7 @@ mod tests { assert_eq!(cavity.shell_count(), 2); assert_eq!(cavity.lumps().unwrap().len(), 1); assert!((crate::measure(&cavity).unwrap().volume - 21.0).abs() < 1e-9); - assert!(!cavity.tessellate(0.01).unwrap().contains(Vec3::new(1.0, 1.5, 2.0))); + assert!(!cavity.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().contains(Vec3::new(1.0, 1.5, 2.0))); let touching = box_faces(Vec3::new(2.0, 3.0, 4.0), Vec3::new(2.0, 3.0, 4.0)); assert!(sew(&outer.iter().chain(&touching).cloned().collect::>()).unwrap_err().to_string().contains("vertex")); let mut hostile = outer.clone(); diff --git a/crates/kernel/src/smlib_sweeps.rs b/crates/kernel/src/smlib_sweeps.rs index 08d525409a..428b6b59ad 100644 --- a/crates/kernel/src/smlib_sweeps.rs +++ b/crates/kernel/src/smlib_sweeps.rs @@ -211,7 +211,7 @@ mod imp { fn roles(body: &Body, start: &Plane, end: Option<&Plane>, edges: &[Seed], inside: &[Seed], rest_is_end: bool) -> Result> { let size = body.size().max(1e-9); let mesh_tol = (size * 2e-4).max(1e-6); - let m = body.tessellate(mesh_tol)?; + let m = body.mesh(&crate::MeshTolerance::chord(mesh_tol))?; let faces = face_tris(&m); let n = body.face_count(); let flat_tol = size * 1e-5 + 1e-9; diff --git a/crates/kernel/src/smooth_edges.rs b/crates/kernel/src/smooth_edges.rs index 90460e3e91..30b3935836 100644 --- a/crates/kernel/src/smooth_edges.rs +++ b/crates/kernel/src/smooth_edges.rs @@ -265,7 +265,7 @@ mod tests { let tool = crate::SplitTool::Curves { plane: Plane::XY, curves: vec![ring], along: None }; let holed = crate::trim(&sheet, &tool, Vec3::new(9., 9., 0.)).unwrap(); assert!(holed.solid.edge_iter().any(|e| e.front().id() == e.back().id()), "a closed edge"); - let mesh = holed.tessellate(0.02).unwrap(); + let mesh = holed.mesh(&crate::MeshTolerance::chord(0.02)).unwrap(); let (smooth, sharp) = classify(&holed, &mesh, WORK); assert_eq!((smooth.len(), sharp.len()), (mesh.edges.len(), mesh.edges.len())); } @@ -273,7 +273,7 @@ mod tests { #[test] fn periodic_ring_hides_only_whole_smooth_wall_joins() { let body = ring(256); - let mesh = body.display_mesh(0.02).unwrap(); + let mesh = body.mesh(&crate::MeshTolerance::chord(0.02)).unwrap(); assert_eq!(mesh.edges.len(), 1536); assert_eq!(mesh.smooth_edges.iter().filter(|f| **f).count(), 512); for (index, (line, smooth)) in mesh.edges.iter().zip(&mesh.smooth_edges).enumerate() { @@ -284,7 +284,7 @@ mod tests { } } // Smoothing flags are display-only: the edges and faces keep the body's identities. - let raw = body.tessellate(0.02).unwrap(); + let raw = body.mesh(&crate::MeshTolerance::chord(0.02)).unwrap(); assert_eq!(mesh.edges.len(), raw.edges.len()); assert_eq!(mesh.edge_faces, raw.edge_faces); for (a, b) in mesh.edges.iter().zip(&raw.edges) { @@ -293,13 +293,13 @@ mod tests { let faces = |m: &Mesh| m.tri_face.iter().copied().collect::>(); assert_eq!(faces(&mesh), faces(&raw)); let moved = crate::transform_matrix(&body, [[1., 0., 0., 0.], [0., 0., 1., 0.], [0., -1., 0., 0.], [30., 20., 10., 1.]]).unwrap(); - assert_eq!(moved.display_mesh(0.02).unwrap().smooth_edges.iter().filter(|f| **f).count(), 512); + assert_eq!(moved.mesh(&crate::MeshTolerance::chord(0.02)).unwrap().smooth_edges.iter().filter(|f| **f).count(), 512); } #[test] fn authored_split_unknown_adjacency_and_exhausted_work_stay_visible() { let body = ring(8); - let mesh = body.display_mesh(0.02).unwrap(); + let mesh = body.mesh(&crate::MeshTolerance::chord(0.02)).unwrap(); let index = mesh.smooth_edges.iter().position(|f| *f).unwrap(); let split = body.clone().with_split_lines(Some(std::sync::Arc::new(vec![mesh.edges[index].clone()]))); assert!(!flags(&split, &mesh).0[index]); @@ -325,7 +325,7 @@ mod tests { p1.x += (p1.y - p0.y) * 1e-6; } let body = crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0., 3.).unwrap().remove(0); - let mesh = body.display_mesh(0.02).unwrap(); + let mesh = body.mesh(&crate::MeshTolerance::chord(0.02)).unwrap(); assert_eq!(mesh.smooth_edges.iter().filter(|f| **f).count(), 7); let crease = mesh .edges @@ -354,7 +354,7 @@ mod tests { let split = crate::split_faces(&body, &[face], &tool).unwrap(); assert!(split.face_count() > body.face_count()); assert!(!split.split_keep().is_empty()); - let mesh = split.display_mesh(0.02).unwrap(); + let mesh = split.mesh(&crate::MeshTolerance::chord(0.02)).unwrap(); let authored: Vec<_> = mesh.edges.iter().enumerate().filter(|(_, line)| crate::heal::on_split_line(line, split.split_keep())).map(|(i, _)| i).collect(); assert!(!authored.is_empty()); diff --git a/crates/kernel/src/step_in/conic_tests.rs b/crates/kernel/src/step_in/conic_tests.rs index 74c6c9f2a0..5e76a3e600 100644 --- a/crates/kernel/src/step_in/conic_tests.rs +++ b/crates/kernel/src/step_in/conic_tests.rs @@ -273,7 +273,7 @@ fn a_cone_bounded_only_by_its_base_circle_reaches_its_point() { let m = measure(body).unwrap(); let exact = PI * r * r * h / 3.0; assert!(rel(m.volume, exact) < 2e-3, "vertex loop {apex_loop}: volume {} vs {exact}", m.volume); - let top = body.tessellate(0.01).unwrap().positions.iter().map(|p| p.z).fold(f64::MIN, f64::max); + let top = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().positions.iter().map(|p| p.z).fold(f64::MIN, f64::max); assert!((top - h).abs() < 1e-6, "vertex loop {apex_loop}: reaches z = {top}, not {h}"); } } diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index 653f3a47b8..30604932f4 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -39,7 +39,7 @@ fn assert_seam_sphere(text: &str, radius: f64) { let want_area = 4.0 * std::f64::consts::PI * radius.powi(2); assert!(rel(m.volume, want_volume) < 1e-3, "volume {} vs {want_volume}", m.volume); assert!(rel(m.area, want_area) < 1e-3, "area {} vs {want_area}", m.area); - let mesh = body.body.display_mesh(radius * 1e-3).unwrap(); + let mesh = body.body.mesh(&crate::MeshTolerance::chord(radius * 1e-3)).unwrap(); assert!(!mesh.positions.is_empty() && !mesh.triangles.is_empty()); } @@ -214,7 +214,7 @@ fn assert_same_import(serial: &StepImport, parallel: &StepImport) { assert_eq!(a.body.paint(), b.body.paint()); assert_eq!(a.body.face_sources, b.body.face_sources); assert_same_measure(&measure(&a.body).unwrap(), &measure(&b.body).unwrap()); - let (a, b) = (a.body.display_mesh(0.05).unwrap(), b.body.display_mesh(0.05).unwrap()); + let (a, b) = (a.body.mesh(&crate::MeshTolerance::chord(0.05)).unwrap(), b.body.mesh(&crate::MeshTolerance::chord(0.05)).unwrap()); assert_eq!(a.positions, b.positions); assert_eq!(a.normals, b.normals); assert_eq!(a.triangles, b.triangles); @@ -794,8 +794,8 @@ fn later_instances_show_the_first_instances_mesh_placed() { // The first instance shows the shared mesh as well (meshing its own would mesh the part twice). let (Some(a), Some(b)) = (first.display_of.as_deref(), second.display_of.as_deref()) else { panic!("both instances share a display") }; assert!(std::sync::Arc::ptr_eq(&a.0, &b.0)); - let a = first.reference_display_mesh_angular(0.01, crate::DISPLAY_ANGLE).unwrap(); - let b = second.reference_display_mesh_angular(0.01, crate::DISPLAY_ANGLE).unwrap(); + let a = first.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(); + let b = second.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(); assert_eq!(a.triangles, b.triangles); assert_eq!(a.tri_face, b.tri_face); assert_eq!(a.edges.len(), b.edges.len()); @@ -1015,7 +1015,7 @@ fn split_band_edges_are_seams() { let curved: f64 = ib.body.faces(1e-3).unwrap().iter().filter(|f| f.plane_normal.is_none()).map(|f| f.area).sum(); let want = std::f64::consts::PI.powi(2) * 30.0; assert!(rel(curved, want) < 2e-3, "{curved} vs {want}"); - let m = ib.body.display_mesh(ib.body.size() * 1e-3).unwrap(); + let m = ib.body.mesh(&crate::MeshTolerance::chord(ib.body.size() * 1e-3)).unwrap(); // Seams: the file's own seam (inside the tube face) and the split edge. assert!(m.seams.iter().filter(|s| **s).count() >= 2, "{:?}", m.seams); } @@ -1682,9 +1682,9 @@ fn explicit_outer_bound_keeps_shallow_cylinder_patch_trimmed() { assert_eq!(b.file_faces, 1); assert!(!b.closed); for mesh in [ - b.body.tessellate(0.01).unwrap(), - b.body.reference_display_mesh_angular(0.01, crate::DISPLAY_ANGLE).unwrap(), - b.body.display_mesh_angular(0.01, crate::DISPLAY_ANGLE).unwrap(), + b.body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(), + b.body.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(), + b.body.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(), ] { assert!(!mesh.triangles.is_empty()); assert!(mesh.bounds().diagonal() < 10.0, "sense={sense}: {:?}", mesh.bounds()); @@ -1697,7 +1697,7 @@ fn explicit_outer_bound_keeps_shallow_cylinder_patch_trimmed() { #[test] fn implicit_bound_can_still_describe_the_complement_of_a_patch() { let imp = step_import(&shallow_cylinder_patch(false, false)).unwrap(); - let mesh = imp.bodies[0].body.tessellate(0.1).unwrap(); + let mesh = imp.bodies[0].body.mesh(&crate::MeshTolerance::chord(0.1)).unwrap(); assert!(mesh.bounds().diagonal() > 5000.0, "without an explicit outer bound the loop is a hole"); } @@ -1730,7 +1730,7 @@ fn explicit_outer_bound_crossing_a_closed_nurbs_seam_stays_trimmed() { let text = text.replacen(old, &surface, 1).replace(TAIL, &format!("{points}{TAIL}")); let imp = step_import(&text).unwrap(); let body = &imp.bodies[0].body; - let mesh = body.reference_display_mesh_angular(0.01, crate::DISPLAY_ANGLE).unwrap(); + let mesh = body.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(); assert!(!mesh.triangles.is_empty(), "{:?}", imp.warnings); assert!(mesh.bounds().diagonal() < 10.0, "sense={sense}: {:?}", mesh.bounds()); assert!((mesh.measure().area - 42.0).abs() < 0.01, "sense={sense}: area {}", mesh.measure().area); @@ -1759,12 +1759,12 @@ fn extruded_ellipse_patch_crossing_profile_seam_stays_trimmed() { assert_eq!(imported.bodies.len(), 1); let body = &imported.bodies[0].body; assert_eq!(imported.bodies[0].file_faces, 1); - let plan = body.display_plan(0.01).unwrap(); + let plan = body.mesh_plan(&crate::MeshTolerance::chord(0.01)).unwrap(); for mesh in [ plan.evaluate(), - body.display_mesh_angular(0.01, crate::DISPLAY_ANGLE).unwrap(), - body.reference_display_mesh_angular(0.01, crate::DISPLAY_ANGLE).unwrap(), - body.tessellate(0.01).unwrap(), + body.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(), + body.mesh(&crate::MeshTolerance { chord: 0.01, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(), + body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(), ] { assert!(!mesh.triangles.is_empty()); assert!(mesh.bounds().diagonal() < 10.0, "radius={radius}, sense={sense}, outer={outer}: {:?}", mesh.bounds()); diff --git a/crates/kernel/src/surface_offset.rs b/crates/kernel/src/surface_offset.rs index a4b523552f..2e34338398 100644 --- a/crates/kernel/src/surface_offset.rs +++ b/crates/kernel/src/surface_offset.rs @@ -400,7 +400,7 @@ mod tests { let cover = crate::patch_edges(&cup, &rim).unwrap(); let face = cover.solid.face_iter().next().unwrap().clone(); let out = offset_surface(&cover, &[0], 1.5, 0.01).unwrap(); - let mesh = out.tessellate(0.05).unwrap(); + let mesh = out.mesh(&crate::MeshTolerance::chord(0.05)).unwrap(); let s = face.surface(); for p in mesh.positions.iter().step_by(7) { let (u, v) = s.search_nearest_parameter(p3(*p), None, 100).unwrap(); diff --git a/crates/kernel/src/surface_ruled.rs b/crates/kernel/src/surface_ruled.rs index c19a1ae98c..d9ebb49e26 100644 --- a/crates/kernel/src/surface_ruled.rs +++ b/crates/kernel/src/surface_ruled.rs @@ -828,7 +828,7 @@ mod tests { let edge = [Vec3::new(14.0, 0.0, z(14.0))]; let s = ruled_surface(&cover, &edge, &ruled(RuledMode::Normal, 3.0, Vec3::new(0.0, 0.0, 12.0))).unwrap(); assert!(s.face_count() == 1 && face_surface(&s, 0).is_none(), "{:?}", face_surface(&s, 0)); - let mesh = s.tessellate(0.01).unwrap(); + let mesh = s.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(); let from_line = |p: &Vec3| Vec3::new(p.x - 14.0, 0.0, p.z - z(14.0)).len(); let far = mesh.positions.iter().map(from_line).fold(0.0, f64::max); assert!((far - 3.0).abs() < 2e-3, "{far}"); diff --git a/crates/kernel/src/surface_trim.rs b/crates/kernel/src/surface_trim.rs index 06b22abaa4..168dece8d4 100644 --- a/crates/kernel/src/surface_trim.rs +++ b/crates/kernel/src/surface_trim.rs @@ -109,7 +109,7 @@ pub fn trim_keep(body: &Body, tool: &SplitTool, keep: TrimKeep) -> Result /// For each point, the face of `body` nearest it (over one tessellation). fn nearest_faces(body: &Body, at: &[Vec3]) -> Result> { - let m = body.tessellate(body.size() * 2e-3)?; + let m = body.mesh(&crate::MeshTolerance::chord(body.size() * 2e-3))?; at.iter() .map(|p| { let mut best: Option<(f64, u32)> = None; @@ -157,7 +157,7 @@ fn cut_by(x: &Body, tool: &SplitTool, keep: TrimKeep, extend: bool, name: &str) let side = ToolSide::of(tool)?; let size = x.size(); let touch = size * 1e-6 + 1e-9; - let m = x.tessellate(size * 2e-3)?; + let m = x.mesh(&crate::MeshTolerance::chord(size * 2e-3))?; let (mut lo, mut hi, mut near) = (f64::INFINITY, f64::NEG_INFINITY, f64::INFINITY); for p in &m.positions { let v = side.at(*p); diff --git a/crates/kernel/src/surfaces.rs b/crates/kernel/src/surfaces.rs index af6dee9d58..63af0a95cf 100644 --- a/crates/kernel/src/surfaces.rs +++ b/crates/kernel/src/surfaces.rs @@ -601,7 +601,7 @@ pub fn remove_faces(body: &Body, faces: &[usize]) -> Result { /// Index of the face nearest a point (over its mesh triangles). pub(crate) fn nearest_face(body: &Body, p: Vec3) -> Result { - let m = body.tessellate(body.size() * 2e-3)?; + let m = body.mesh(&crate::MeshTolerance::chord(body.size() * 2e-3))?; let mut best: Option<(f64, u32)> = None; for (t, f) in m.triangles.iter().zip(&m.tri_face) { let Some([a, b, c]) = m.tri(t) else { continue }; @@ -1467,7 +1467,7 @@ mod tests { // Δφ·[R((r+1)² − r²)/2·Δθ + ((r+1)³ − r³)/3·(sin θ1 − sin θ0)]. let ring = torus(Vec3::ZERO, 20.0, 5.0).unwrap(); let face = copy_faces(&ring, &[Vec3::new(25.0, 0.0, 0.0)]).unwrap(); - let mesh = face.tessellate(0.01).unwrap(); + let mesh = face.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(); let angles: Vec<(f64, f64)> = mesh.positions.iter().map(|p| (p.z.atan2((p.x * p.x + p.y * p.y).sqrt() - 20.0), p.y.atan2(p.x))).collect(); let span = |f: &dyn Fn(&(f64, f64)) -> f64| { let (lo, hi) = angles.iter().map(f).fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), x| (a.min(x), b.max(x))); diff --git a/crates/kernel/src/sweep_path.rs b/crates/kernel/src/sweep_path.rs index 8b0ec78e28..da6865ada1 100644 --- a/crates/kernel/src/sweep_path.rs +++ b/crates/kernel/src/sweep_path.rs @@ -814,7 +814,7 @@ fn smooth_closed_walls(plane: &Plane, profile: &NumberedLoop, st: &[Station], of sides.extend(smooth_loft_faces(wires.get(middle..).ok_or_else(fail)?, numbers.get(middle..).ok_or_else(fail)?, true)?); let b = close_sweep(sides)?; let tol = (profile.lp.signed_area().abs().sqrt() * 0.05).max(1e-3); - if b.tessellate(tol).is_ok_and(|m| (m.measure().volume - expected).abs() <= 0.15 * expected.abs()) { Ok(b) } else { Err(fail()) } + if b.mesh(&crate::MeshTolerance::chord(tol)).is_ok_and(|m| (m.measure().volume - expected).abs() <= 0.15 * expected.abs()) { Ok(b) } else { Err(fail()) } }) } @@ -861,7 +861,7 @@ fn smooth_walls(plane: &Plane, profile: &NumberedLoop, st: &[Station], expected: let 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(tol).is_ok_and(|m| (m.measure().volume - expected).abs() <= 0.15 * expected.abs()) + && b.mesh(&crate::MeshTolerance::chord(tol)).is_ok_and(|m| (m.measure().volume - expected).abs() <= 0.15 * expected.abs()) { return Ok(b); } @@ -895,7 +895,7 @@ mod tests { let plane = Plane::from_normal(path[0], t0).unwrap(); let b = sweep_path(&plane, &circle(2.0), &path).unwrap(); let len: f64 = path.windows(2).map(|w| w[0].dist(w[1])).sum(); - let v = b.tessellate(0.01).unwrap().measure().volume; + let v = b.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().volume; let want = std::f64::consts::PI * 4.0 * len; assert!((v - want).abs() < 0.02 * want, "{v} vs {want}"); // One wall per profile segment and two caps (ruled walls would give dozens). @@ -906,7 +906,7 @@ mod tests { let sq = |h: f64| Loop2::polygon(&[Vec2::new(-h, -h), Vec2::new(h, -h), Vec2::new(h, h), Vec2::new(-h, h)]); let region = Region2 { outer: sq(3.0), holes: vec![sq(1.0).reversed()] }; let b = sweep_path(&plane, ®ion, &path).unwrap(); - let v = b.tessellate(0.01).unwrap().measure().volume; + let v = b.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().volume; let want = 32.0 * 40.0; assert!((v - want).abs() < 0.03 * want, "{v} vs {want}"); assert!(sweep_path(&plane, ®ion, &[Vec3::ZERO]).is_err()); @@ -929,14 +929,14 @@ mod tests { let small = big * k; let ctl = SweepControl { scale: k, ..SweepControl::default() }; let b = crate::sweep_with(&Plane::XY, &circle(big), &up(h), &ctl).unwrap(); - let m = b.tessellate(0.005).unwrap().measure(); + let m = b.mesh(&crate::MeshTolerance::chord(0.005)).unwrap().measure(); let volume = PI * h * (big * big + big * small + small * small) / 3.0; let lateral = PI * (big + small) * (h * h + (big - small).powi(2)).sqrt(); let caps = PI * (big * big + small * small); assert!((m.volume - volume).abs() < 2e-3 * volume, "volume {} vs {volume}", m.volume); assert!((m.area - caps - lateral).abs() < 2e-3 * lateral, "lateral {} vs {lateral}", m.area - caps); // The end cap is the scaled circle. - let top = b.tessellate(0.005).unwrap().positions.iter().filter(|p| (p.z - h).abs() < 1e-6).map(|p| p.x.hypot(p.y)).fold(0.0, f64::max); + let top = b.mesh(&crate::MeshTolerance::chord(0.005)).unwrap().positions.iter().filter(|p| (p.z - h).abs() < 1e-6).map(|p| p.x.hypot(p.y)).fold(0.0, f64::max); assert!((top - small).abs() < 1e-3, "end radius {top}"); // The surface sweep of the circle's curve: the frustum's lateral face alone. let sheet = crate::sweep_sheet_with(&Plane::XY, std::slice::from_ref(&circle(big).outer.segs), &up(h), &ctl).unwrap(); @@ -946,7 +946,7 @@ mod tests { /// The corners of a twisted square: each seam edge's angle at `z`. fn corner_angles(b: &crate::Body, z: f64) -> Vec { - let mesh = b.tessellate(0.002).unwrap(); + let mesh = b.mesh(&crate::MeshTolerance::chord(0.002)).unwrap(); let mut out = Vec::new(); for e in &mesh.edges { for w in e.windows(2) { @@ -968,7 +968,7 @@ mod tests { let ctl = SweepControl { twist, ..SweepControl::default() }; let b = crate::sweep_with(&Plane::XY, &square(5.0), &up(h), &ctl).unwrap(); // Twisted walls are saddles: a fine mesh to measure them. - let v = b.tessellate(0.0005).unwrap().measure().volume; + let v = b.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap().measure().volume; assert!((v - 100.0 * h).abs() < 2e-3 * 100.0 * h, "volume {v}"); // Corners start at 45 deg (and 135, -45, -135): each seam turns by the twist so far. let off = |a: f64, want: f64| { @@ -1005,7 +1005,7 @@ mod tests { let rise = r * FRAC_PI_4.sin(); let ctl = SweepControl { orientation: SweepOrientation::Parallel, ..SweepControl::default() }; let b = sweep_path_with(&Plane::XY, &square(3.0), &path, &ctl).unwrap(); - let mesh = b.tessellate(0.005).unwrap(); + let mesh = b.mesh(&crate::MeshTolerance::chord(0.005)).unwrap(); let v = mesh.measure().volume; let want = 36.0 * rise; assert!((v - want).abs() < 5e-3 * want, "volume {v} vs {want}"); @@ -1036,7 +1036,7 @@ mod tests { .collect(); let ctl = SweepControl { orientation: SweepOrientation::Direction(Vec3::Y), ..SweepControl::default() }; let b = sweep_path_with(&Plane::XY, &square(3.0), &path, &ctl).unwrap(); - let mesh = b.tessellate(0.005).unwrap(); + let mesh = b.mesh(&crate::MeshTolerance::chord(0.005)).unwrap(); let len = r * FRAC_PI_2; assert!((mesh.measure().volume - 36.0 * len).abs() < 5e-3 * 36.0 * len); // The sides facing ±Y stay at y = ±3. diff --git a/crates/kernel/src/tangent_cylinders.rs b/crates/kernel/src/tangent_cylinders.rs index 517af2bc1b..a5505eb4df 100644 --- a/crates/kernel/src/tangent_cylinders.rs +++ b/crates/kernel/src/tangent_cylinders.rs @@ -126,7 +126,7 @@ fn next_split(a: &Body, b: &Body, tol: f64) -> Option<(bool, usize, Plane)> { for y in &in_b { let Some(crossing) = crossing(x, y, tol) else { continue }; if meshes.is_none() { - meshes = Some((a.tessellate(chord).ok()?, b.tessellate(chord).ok()?)); + meshes = Some((a.mesh(&crate::MeshTolerance::chord(chord)).ok()?, b.mesh(&crate::MeshTolerance::chord(chord)).ok()?)); } let (ma, mb) = meshes.as_ref()?; for p in crossing.points { diff --git a/crates/kernel/src/taper_mesh_tests.rs b/crates/kernel/src/taper_mesh_tests.rs index 25c1aceaee..2b46d62632 100644 --- a/crates/kernel/src/taper_mesh_tests.rs +++ b/crates/kernel/src/taper_mesh_tests.rs @@ -36,7 +36,7 @@ fn collapsed_taper_mesh_stays_accurate_as_tolerance_decreases() { let wanted = expected(&law); for tol in [0.002, 0.001, 0.0005, 0.0002] { for refine in [false, true] { - let mesh = body.tessellate_with(tol, refine).unwrap(); + let mesh = body.mesh(&crate::MeshTolerance::chord(tol)).unwrap(); assert!(mesh.positions.iter().all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite())); let got = mesh.measure().volume; assert!((got - wanted).abs() / wanted < 1e-4, "tol={tol}, refine={refine}: {got} vs {wanted}"); diff --git a/crates/kernel/src/tess/tests.rs b/crates/kernel/src/tess/tests.rs index 14a2829a9d..4a7dbdb1ec 100644 --- a/crates/kernel/src/tess/tests.rs +++ b/crates/kernel/src/tess/tests.rs @@ -117,7 +117,7 @@ fn holes_and_trimmed_faces_are_watertight() { assert_watertight(&m, "cross hole"); assert_eq!(stats.delaunay, 0, "{stats:?}"); // Against the reference mesher's volume (no closed form for the two-cylinder intersection). - let Ok(reference) = body.tessellate(tol * 0.2) else { panic!("reference mesh") }; + let Ok(reference) = body.mesh(&crate::MeshTolerance::chord(tol * 0.2)) else { panic!("reference mesh") }; let (a, b) = (m.measure().volume, reference.measure().volume); assert!((a - b).abs() < 0.01 * b, "cross hole volume {a} vs {b}"); } @@ -126,7 +126,7 @@ fn holes_and_trimmed_faces_are_watertight() { fn faces_and_edges_keep_the_body_order() { let body = ok(crate::cylinder(Vec3::ZERO, Vec3::Z, 2.0, 5.0)); let (m, _) = plan_body(&body, 0.01); - let Ok(reference) = body.tessellate(0.01) else { panic!("reference mesh") }; + let Ok(reference) = body.mesh(&crate::MeshTolerance::chord(0.01)) else { panic!("reference mesh") }; assert_eq!(m.edges.len(), reference.edges.len()); assert_eq!(m.edge_faces, reference.edge_faces); // Same faces in the same order: each face's area agrees with the reference mesher's. @@ -173,7 +173,7 @@ fn render_wireframes() { ]; for (name, body) in shapes { let t0 = std::time::Instant::now(); - let Ok(old) = body.tessellate(tol) else { panic!("reference") }; + let Ok(old) = body.mesh(&crate::MeshTolerance::chord(tol)) else { panic!("reference") }; let t_old = t0.elapsed().as_secs_f64() * 1e3; let t0 = std::time::Instant::now(); let new = match plan(&body.solid, tol, Some(crate::tess::size::Turn { angle: crate::DISPLAY_ANGLE, floor: tol * 1e-3 }), body.boundary_tol) { @@ -491,8 +491,8 @@ fn max_chord(b: &Body, m: &Mesh) -> f64 { /// The display plan of `b` at `tol` (and the display angle), evaluated: closed, with no /// zero-area or needle triangle. fn closed_display_mesh(name: &str, b: &Body, tol: f64) -> Mesh { - let plan = ok(b.display_plan_angular(tol, crate::DISPLAY_ANGLE)); - let m = ok(b.display_mesh_of(&plan)); + let plan = ok(b.mesh_plan(&crate::MeshTolerance { chord: tol, angle: Some(crate::DISPLAY_ANGLE) })); + let m = ok(b.mesh_of(&plan)); assert_watertight(&m, &format!("{name} at {tol}")); for t in &m.triangles { let [a, b, c] = t.map(|i| m.positions[i as usize]); @@ -653,7 +653,7 @@ fn a_modelled_thread_in_a_bore_meshes_inside_its_block() { let threaded = smlib_cut(&smlib_cut(&block, &bore), &smlib_groove(iso_groove(1.0, 8.0, 3.5, true), -1.0, 1.0, 12.0)); let body = ok(crate::native::body_from_smlib(&threaded)); assert!(seam::turns_more_than_once(&body.solid), "the groove's walls wind round the bore"); - let m = ok(body.tessellate(0.01)); + let m = ok(body.mesh(&crate::MeshTolerance::chord(0.01))); let (lo, hi) = m .positions .iter() @@ -667,7 +667,7 @@ fn a_modelled_thread_in_a_bore_meshes_inside_its_block() { let m = closed_display_mesh("thread in a bore", &body, tol); let chord = max_chord(&body, &m); assert!(chord <= tol * 1.05, "thread in a bore at {tol}: chord {chord}"); - assert_faces_lie_flat("thread in a bore", &ok(body.display_plan_angular(tol, crate::DISPLAY_ANGLE))); + assert_faces_lie_flat("thread in a bore", &ok(body.mesh_plan(&crate::MeshTolerance { chord: tol, angle: Some(crate::DISPLAY_ANGLE) }))); } } @@ -682,7 +682,7 @@ fn a_thread_root_band_running_out_through_an_end_face_lies_on_its_surface() { let Ok(shaft) = solidcraft_smlib::Brep::make_cylinder([0.0; 3], 5.0, 30.0) else { panic!("shaft") }; let threaded = smlib_cut(&shaft, &smlib_groove(iso_groove(1.5, 10.0, 5.0, false), -1.5, 1.5, 11.0)); let body = ok(crate::native::body_from_smlib(&threaded)); - let m = ok(body.tessellate(0.01)); + let m = ok(body.mesh(&crate::MeshTolerance::chord(0.01))); let (lo, hi) = m .positions .iter() @@ -693,7 +693,7 @@ fn a_thread_root_band_running_out_through_an_end_face_lies_on_its_surface() { let chord = max_chord(&body, &m); assert!(chord <= tol * 1.05, "thread run out at {tol}: chord {chord}"); assert!(m.triangles.len() < 60_000, "thread run out at {tol}: {} triangles", m.triangles.len()); - assert_faces_lie_flat("thread run out", &ok(body.display_plan_angular(tol, crate::DISPLAY_ANGLE))); + assert_faces_lie_flat("thread run out", &ok(body.mesh_plan(&crate::MeshTolerance { chord: tol, angle: Some(crate::DISPLAY_ANGLE) }))); } } diff --git a/crates/kernel/src/tests.rs b/crates/kernel/src/tests.rs index 66a608e8ae..20193cfe0c 100644 --- a/crates/kernel/src/tests.rs +++ b/crates/kernel/src/tests.rs @@ -65,7 +65,7 @@ fn through_cut_preserves_a_larger_blind_pocket() { let m = measure(&result).unwrap(); assert!(rel(m.volume, expected) < 5e-4, "reverse={reverse}: {} vs {expected}", m.volume); assert_eq!(m.merged.faces, 9); - assert!(!result.tessellate(0.01).unwrap().contains(Vec3::new(10.0, 0.0, 3.0))); + assert!(!result.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().contains(Vec3::new(10.0, 0.0, 3.0))); } } @@ -718,7 +718,7 @@ fn loft_and_sweep() { ]; let t = sweep(&Plane::XY, &disc, &path).unwrap(); for tol in [0.1, 0.01, 0.003, 0.001] { - println!("tube tess {tol}: {}", t.tessellate(tol).unwrap().measure().volume); + println!("tube tess {tol}: {}", t.mesh(&crate::MeshTolerance::chord(tol)).unwrap().measure().volume); } let area = PI * 9.0; let expect = area * (30.0 + 30.0 + 20.0 * std::f64::consts::FRAC_PI_2); @@ -1032,11 +1032,11 @@ fn similar_spheres_meet() { fn debug_convergence() { let b = extrude(&Plane::XY, &[Region2 { outer: Loop2::circle(Vec2::ZERO, 15.0), holes: vec![] }], 0.0, 50.0).unwrap().pop().unwrap(); for t in [0.1, 0.05, 0.02, 0.01, 0.005, 0.002] { - let m = b.tessellate(t).unwrap(); + let m = b.mesh(&crate::MeshTolerance::chord(t)).unwrap(); let mm = m.measure(); println!("tol {t}: tris {} vol {} err {:.3e}", m.triangles.len(), mm.volume, mm.volume / (PI * 225.0 * 50.0) - 1.0); } - let m = b.tessellate(0.05).unwrap(); + let m = b.mesh(&crate::MeshTolerance::chord(0.05)).unwrap(); for f in 0..4u32 { let ns: Vec = m.triangles.iter().zip(&m.tri_face).filter(|(_, ff)| **ff == f).flat_map(|(t, _)| t.iter().map(|k| m.normals[*k as usize])).collect(); @@ -1152,7 +1152,7 @@ fn chamfer_sloped_basin_rim() { assert_eq!(after.merged.faces, before.merged.faces + 1); assert_eq!(result.lumps().unwrap().len(), 1); assert!(result.validity().is_empty()); - let mesh = result.tessellate(0.01).unwrap(); + let mesh = result.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(); assert!(!mesh.contains(Vec3::new(50.0, 3.5, 29.9))); assert!(mesh.contains(Vec3::new(50.0, 3.5, 28.0))); } @@ -1194,7 +1194,7 @@ fn debug_chamfer_cut() { println!("face {} area {:.3} c {:?} n {:?}", f.index, f.area, f.centroid, f.plane_normal); } for tol in [0.1, 0.01, 0.001] { - println!("tess {tol}: {}", r.tessellate(tol).unwrap().measure().volume); + println!("tess {tol}: {}", r.mesh(&crate::MeshTolerance::chord(tol)).unwrap().measure().volume); } } @@ -1402,7 +1402,7 @@ fn debug_cube_sphere_measure() { let r = guard("t", || Ok(truck_shapeops::and(&b.deep_copy(), &s.deep_copy(), 0.05))).unwrap().unwrap(); let body = Body::new(r).unwrap(); for tol in [0.5, 0.1, 0.02] { - let m = body.tessellate(tol).unwrap(); + let m = body.mesh(&crate::MeshTolerance::chord(tol)).unwrap(); let mm = m.measure(); println!("tol {tol}: vol {} area {} tris {}", mm.volume, mm.area, m.triangles.len()); } @@ -1979,7 +1979,7 @@ fn fillet_an_open_curved_edge() { let f = fillet(&b, &[Vec3::new(0.0, 10.0, 15.0)], 2.0).unwrap(); // (A fine tessellation: the difference of two measured volumes magnifies their error.) let _ = v0; - let fine = |x: &Body| x.tessellate_with(0.002, true).unwrap().measure().volume; + let fine = |x: &Body| x.mesh(&crate::MeshTolerance::chord(0.002)).unwrap().measure().volume; let removed = fine(&b) - fine(&f); // Pappus: the corner's cross-section r²(1 − π/4), its centroid 0.2234 r in from the wall. let r = 2.0; @@ -2019,7 +2019,7 @@ fn fillet_smooth_arc_and_line_loop_keeps_exact_removal() { let area = r * r * (1.0 - PI / 4.0); let centroid = r * (10.0 - 3.0 * PI) / (3.0 * (4.0 - PI)); let want = area * (40.0 + 2.0 * PI * (10.0 - centroid)); - let fine = |b: &Body| b.tessellate_with(1e-4, true).unwrap().measure().volume; + let fine = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume; let removed = fine(&body) - fine(&blended); assert!(rel(removed, want) < 1e-3, "{removed} vs {want}"); } @@ -2057,7 +2057,7 @@ fn fillet_rounded_rectangle_with_tiny_end_edges() { let area = radius * radius * (1.0 - PI / 4.0); let centroid = radius * (10.0 - 3.0 * PI) / (3.0 * (4.0 - PI)); let removed = area * (2.0 * (w + h - 4.0 * r) + 2.0 * PI * (r - centroid)); - let fine = |b: &Body| b.tessellate_with(1e-4, true).unwrap().measure().volume; + let fine = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume; let initial = fine(&body); // Every rim edge, the tiny end alone (tangent chain), and the opposite selection order. for selection in [picks.to_vec(), vec![picks[3]], picks.into_iter().rev().collect()] { @@ -2084,7 +2084,7 @@ fn fillet_sharp_arc_and_arc_loop() { let body = extrude(&Plane::XY, &[Region2 { outer: outline, holes: vec![] }], 0.0, 15.0).unwrap().pop().unwrap(); let blended = fillet(&body, &[Vec3::new(5.0, 0.0, 15.0), Vec3::new(-5.0, 0.0, 15.0)], 0.5).unwrap(); assert!(blended.validity().is_empty(), "{:?}", blended.validity()); - let mesh = blended.tessellate_with(1e-4, true).unwrap(); + let mesh = blended.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap(); for p in &mesh.positions { assert!((p.x - 5.0).hypot(p.y) <= 10.0 + 1e-3 && (p.x + 5.0).hypot(p.y) <= 10.0 + 1e-3, "outside the lens: {p:?}"); } @@ -2096,7 +2096,7 @@ fn fillet_sharp_arc_and_arc_loop() { (area(10.0) - area(10.0 - 0.5 * (1.0 - theta.cos()))) * 0.5 * theta.cos() * dt }) .sum(); - let removed = body.tessellate_with(1e-4, true).unwrap().measure().volume - mesh.measure().volume; + let removed = body.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume - mesh.measure().volume; assert!(rel(removed, want) < 1e-3, "{removed} vs {want}"); } @@ -2138,7 +2138,7 @@ fn fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole() { let picks: Vec<_> = edges.iter().enumerate().filter(|(i, _)| mask & (1 << i) != 0).map(|(_, p)| *p).collect(); let blended = fillet(&inner, &picks, 0.5).unwrap_or_else(|e| panic!("{picks:?}: {e}")); assert!(blended.validity().is_empty(), "{:?}", blended.validity()); - let mesh = blended.display_mesh(0.01).unwrap(); + let mesh = blended.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(); for p in &mesh.positions { let inside = p.x.hypot(p.y) <= 25.001 || (p.x.abs() <= 4.001 && p.y >= y - 0.001 && p.y <= corner_y) @@ -2194,9 +2194,9 @@ fn fillet_mixed_arc_and_line_perimeter_edges() { assert!(blended.validity().is_empty(), "{:?}", blended.validity()); let display_tol = (blended.size() * 1e-3).max(1e-3); for mesh in [ - blended.tessellate_with(1e-4, true).unwrap(), - blended.display_mesh_angular(display_tol, crate::DISPLAY_ANGLE).unwrap(), - blended.reference_display_mesh_angular(display_tol, crate::DISPLAY_ANGLE).unwrap(), + blended.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap(), + blended.mesh(&crate::MeshTolerance { chord: display_tol, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(), + blended.mesh(&crate::MeshTolerance { chord: display_tol, angle: Some(crate::DISPLAY_ANGLE) }).unwrap(), ] { let bounds = mesh.bounds(); let tol = 1e-3; @@ -2236,7 +2236,7 @@ fn fillet_mixed_arc_and_line_removed_volume_converges() { let body = mixed_arc_and_line_prism(); let source_exact = PI * 100.0 * 15.0 / 2.0; let tolerances = [1e-4, 2.5e-5]; - let source_volumes = tolerances.map(|tol| body.tessellate_with(tol, true).unwrap().measure().volume); + let source_volumes = tolerances.map(|tol| body.mesh(&crate::MeshTolerance::chord(tol)).unwrap().measure().volume); for r in [0.5, 0.25] { let want = d_fillet_removed_reference(r, 8192); assert!((want - d_fillet_removed_reference(r, 4096)).abs() < 1e-7, "reference quadrature must converge"); @@ -2246,7 +2246,7 @@ fn fillet_mixed_arc_and_line_removed_volume_converges() { let blended = fillet(&body, &[Vec3::new(0.0, 10.0, 15.0), Vec3::new(5.0, 0.0, 15.0)], r).unwrap(); let mut previous = None; for (tol, source) in tolerances.into_iter().zip(source_volumes) { - let volume = blended.tessellate_with(tol, true).unwrap().measure().volume; + let volume = blended.mesh(&crate::MeshTolerance::chord(tol)).unwrap().measure().volume; // Equal chord tolerances reduce (not necessarily cancel) the error from the // unchanged walls. The convergence and exact-source checks below guard this. let removed = source - volume; @@ -2289,7 +2289,7 @@ fn loop_chamfer_mixed_arc_and_line_uses_offset_intersections() { }) .sum(); // The cone's triangulation needs this chord tolerance for the small volume difference. - let fine = |b: &Body| b.tessellate_with(2.5e-5, true).unwrap().measure().volume; + let fine = |b: &Body| b.mesh(&crate::MeshTolerance::chord(2.5e-5)).unwrap().measure().volume; let removed = fine(&body) - fine(&blended); assert!(rel(removed, want) < 1e-3, "{removed} vs {want}"); } @@ -2337,7 +2337,7 @@ fn fillet_partial_mixed_arc_and_line_perimeter_run() { let body = partial_mixed_run_prism(); let blended = fillet(&body, &[Vec3::new(35.0, 20.0, 15.0), Vec3::new(20.0, 30.0, 15.0), Vec3::new(5.0, 20.0, 15.0)], 0.5).unwrap(); assert!(blended.validity().is_empty(), "{:?}", blended.validity()); - let mesh = blended.tessellate_with(1e-4, true).unwrap(); + let mesh = blended.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap(); let bounds = mesh.bounds(); let tol = 1e-3; assert!( @@ -2719,7 +2719,7 @@ fn orientation_mesh_reuses_missing_face_check_at_matching_tolerance() { for scale in [0.001, 1.0, 1000.0] { let body = box_solid(Vec3::ZERO, Vec3::new(10., 20., 30.) * scale).unwrap(); let tol = (body.size() * 0.01).max(1e-3); - let mesh = body.tessellate(tol).unwrap(); + let mesh = body.mesh(&crate::MeshTolerance::chord(tol)).unwrap(); let mut present = vec![false; body.face_count()]; for face in mesh.tri_face { present[face as usize] = true; diff --git a/crates/kernel/src/thin_face_tests.rs b/crates/kernel/src/thin_face_tests.rs index 6107619a97..caf0ae1234 100644 --- a/crates/kernel/src/thin_face_tests.rs +++ b/crates/kernel/src/thin_face_tests.rs @@ -41,7 +41,7 @@ fn a_lens_thinner_than_the_tolerance_is_meshed() { assert!(union.validity().is_empty(), "{:?}", union.validity()); let size = union.size(); for tol in [size * 1e-2, size * 2.5e-3, size * 1e-3] { - let mesh = union.tessellate(tol).unwrap(); + let mesh = union.mesh(&crate::MeshTolerance::chord(tol)).unwrap(); let faces: std::collections::HashSet = mesh.tri_face.iter().copied().collect(); assert_eq!(faces.len(), union.face_count(), "faces with triangles at {tol}"); assert_eq!(open_sides(&mesh), 0, "open sides at {tol}"); @@ -96,7 +96,7 @@ fn sphere_touching_a_box_edge_unites_soundly() { assert!(union.validity().is_empty(), "{:?}", union.validity()); let size = union.size(); for tol in [size * 1e-2, size * 2.5e-3, size * 6e-4] { - let faces: std::collections::HashSet = union.tessellate(tol).unwrap().tri_face.into_iter().collect(); + let faces: std::collections::HashSet = union.mesh(&crate::MeshTolerance::chord(tol)).unwrap().tri_face.into_iter().collect(); assert_eq!(faces.len(), union.face_count(), "at {tol}"); } } diff --git a/crates/kernel/src/thread_display_tests.rs b/crates/kernel/src/thread_display_tests.rs index 2e74a35ea9..390bb2d81d 100644 --- a/crates/kernel/src/thread_display_tests.rs +++ b/crates/kernel/src/thread_display_tests.rs @@ -200,7 +200,7 @@ fn threads() -> Vec<(String, Body, f64, f64, f64)> { fn thread_display_mesh_stays_on_its_surfaces() { for (name, b, d, p, turns) in threads() { let tol = tol(&b); - let plan = b.display_plan_angular(tol, DISPLAY_ANGLE).unwrap_or_else(|e| panic!("{name}: no display plan: {e}")); + let plan = b.mesh_plan(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap_or_else(|e| panic!("{name}: no display plan: {e}")); let mesh = plan.evaluate(); let q = quality(&plan, &mesh); assert!(q.worst_sag <= 1.5 * tol, "{name}: a triangle lies {} mm off its surface (tolerance {tol})", q.worst_sag); @@ -226,9 +226,9 @@ fn thread_display_mesh_stays_on_its_surfaces() { let want = ridge_volume(d, p, turns); assert!((m.volume - want).abs() < m.area * tol, "{name}: display mesh volume {}, the thread's {want}", m.volume); // The reference mesh (picking, measuring, the viewport without GPU tessellation) is the same. - let reference = b.reference_display_mesh_angular(tol, DISPLAY_ANGLE).unwrap(); + let reference = b.mesh(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap(); assert_eq!(reference.triangles.len(), mesh.triangles.len(), "{name}"); - let measured = b.tessellate(tol * 0.1).unwrap().measure().volume; + let measured = b.mesh(&crate::MeshTolerance::chord(tol * 0.1)).unwrap().measure().volume; assert!((measured - want).abs() < want * 1e-3, "{name}: measured volume {measured}, the thread's {want}"); } } @@ -238,7 +238,7 @@ fn thread_display_mesh_stays_on_its_surfaces() { #[test] fn thread_display_mesh_has_no_degenerate_or_flipped_triangles() { for (name, b, ..) in threads() { - let plan = b.display_plan_angular(tol(&b), DISPLAY_ANGLE).unwrap_or_else(|e| panic!("{name}: no display plan: {e}")); + let plan = b.mesh_plan(&crate::MeshTolerance { chord: tol(&b), angle: Some(DISPLAY_ANGLE) }).unwrap_or_else(|e| panic!("{name}: no display plan: {e}")); let mesh = plan.evaluate(); let q = quality(&plan, &mesh); assert!(q.tris > 1000, "{name}: {} triangles", q.tris); diff --git a/crates/kernel/src/thread_tests.rs b/crates/kernel/src/thread_tests.rs index 38c53699c9..08f7218b44 100644 --- a/crates/kernel/src/thread_tests.rs +++ b/crates/kernel/src/thread_tests.rs @@ -18,7 +18,7 @@ fn faceted_cut_of_a_helical_groove() { let section = [Vec2::new(1.2, -0.06), Vec2::new(1.6, -0.06), Vec2::new(1.6, 0.06), Vec2::new(1.2, 0.06)]; let groove = sweep_helix_mesh(&plane, §ion, base, k, p, 5.0, 48).unwrap(); let cut = faceted_boolean(&shaft, &groove, BoolOp::Cut, 1e-3).unwrap().unwrap(); - let m = cut.tessellate(0.01).unwrap().measure(); + let m = cut.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure(); // In the shaft from z = 0: the section (r 1.2…1.5, 0.12 tall) for 4 turns, plus the part of // the first turn's section above z = 0 (its z range is ±0.06 about the turn's height). let per_turn = 2.0 * PI * 0.12 * (1.5f64.powi(2) - 1.2f64.powi(2)) / 2.0; @@ -32,14 +32,14 @@ fn faceted_cut_into_a_bore() { let nut = crate::box_solid(Vec3::new(-3.0, -3.0, 0.0), Vec3::new(3.0, 3.0, 2.0)).unwrap(); let bore = cylinder(Vec3::new(0.0, 0.0, -1.0), Vec3::new(0.0, 0.0, 1.0), 1.23, 4.0).unwrap(); let nut = crate::boolean(&nut, &bore, BoolOp::Cut).unwrap().unwrap(); - let v0 = nut.tessellate(1e-3).unwrap().measure().volume; + let v0 = nut.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().measure().volume; let (p, k) = (0.5, Vec3::new(0.0, 0.0, 1.0)); let base = Vec3::new(0.0, 0.0, -p); let plane = Plane { origin: base, x: Vec3::new(1.0, 0.0, 0.0), y: k }; let section = [Vec2::new(1.1, -0.1), Vec2::new(1.5, -0.1), Vec2::new(1.5, 0.1), Vec2::new(1.1, 0.1)]; let groove = sweep_helix_mesh(&plane, §ion, base, k, p, 6.0, 48).unwrap(); let cut = faceted_boolean(&nut, &groove, BoolOp::Cut, 2.5e-3).unwrap().unwrap(); - let removed = v0 - cut.tessellate(1e-3).unwrap().measure().volume; + let removed = v0 - cut.mesh(&crate::MeshTolerance::chord(1e-3)).unwrap().measure().volume; // Four turns of the section's part outside the bore (r 1.23…1.5, 0.2 tall). let want = 4.0 * PI * 0.2 * (1.5f64.powi(2) - 1.23f64.powi(2)); assert!((removed - want).abs() < 0.03 * want, "{removed} vs {want}"); @@ -58,7 +58,7 @@ fn a_long_thread_cuts_without_a_deep_tree() { let section = [Vec2::new(3.3, -0.08), Vec2::new(4.1, -0.5), Vec2::new(4.1, 0.5), Vec2::new(3.3, 0.08)]; let groove = sweep_helix_mesh(&plane, §ion, base, k, p, len / p + 2.0, 48).unwrap(); let cut = faceted_boolean(&shaft, &groove, BoolOp::Cut, p * 0.005).unwrap().unwrap(); - let v = cut.tessellate(0.01).unwrap().measure().volume; + let v = cut.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().measure().volume; let v0 = PI * r * r * len; assert!(v < v0 * 0.95 && v > v0 * 0.6, "{v} of {v0}"); } diff --git a/crates/kernel/src/topo.rs b/crates/kernel/src/topo.rs index eb4dece08a..41e81bcbff 100644 --- a/crates/kernel/src/topo.rs +++ b/crates/kernel/src/topo.rs @@ -393,7 +393,7 @@ fn classify_faces(b: &Body, mesh: &Mesh, nf: usize, tol: f64) -> Vec { .collect() } -/// Merged face/edge/vertex counts (see module docs). `mesh` must be `b.tessellate(..)`. +/// Merged face/edge/vertex counts (see module docs). `mesh` must be `b.mesh(&crate::MeshTolerance::chord(..))`. /// The point halfway along a polyline (by length). fn halfway(pl: &[Vec3]) -> Option { let total: f64 = pl.windows(2).map(|w| w[0].dist(w[1])).sum(); @@ -646,7 +646,7 @@ pub struct CylinderFace { pub fn cylinder_face_at(b: &Body, p: Vec3) -> Option { let size = b.size(); let tol = (size * 1e-3).max(1e-3); - let mesh = b.tessellate(tol).ok()?; + let mesh = b.mesh(&crate::MeshTolerance::chord(tol)).ok()?; // Each face's distance from the point (its nearest triangle). let mut near: HashMap = HashMap::new(); for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face) { @@ -706,7 +706,7 @@ impl CylinderFace { pub fn cylinder_end_at(b: &Body, p: Vec3) -> Option { let size = b.size(); let tol = (size * 1e-3).max(1e-3); - let mesh = b.tessellate(tol).ok()?; + let mesh = b.mesh(&crate::MeshTolerance::chord(tol)).ok()?; let surfs = classify_faces(b, &mesh, b.face_count(), (size * 1e-4).max(1e-6)); let mut best: Option<(f64, CylinderFace)> = None; for (face, s) in surfs.iter().enumerate() { diff --git a/crates/kernel/src/trimmed_face_mesh_tests.rs b/crates/kernel/src/trimmed_face_mesh_tests.rs index 248897d52e..faa5cdf0a6 100644 --- a/crates/kernel/src/trimmed_face_mesh_tests.rs +++ b/crates/kernel/src/trimmed_face_mesh_tests.rs @@ -67,8 +67,8 @@ fn trimmed_ruled_loft_faces_mesh_on_their_surfaces() { let shell = cylinder(Vec3::new(0.0, -36.5, 0.0), Vec3::Y, 23.0, 73.0).unwrap(); let joined = boolean(&tube, &shell, BoolOp::Union).unwrap().unwrap(); let tol = (joined.size() * 1e-3).max(1e-3); - let display = per_face(&joined, &joined.reference_display_mesh_angular(tol, DISPLAY_ANGLE).unwrap()); - let fine = per_face(&joined, &joined.tessellate(tol / 8.0).unwrap()); + let display = per_face(&joined, &joined.mesh(&crate::MeshTolerance { chord: tol, angle: Some(DISPLAY_ANGLE) }).unwrap()); + let fine = per_face(&joined, &joined.mesh(&crate::MeshTolerance::chord(tol / 8.0)).unwrap()); for (fi, ((area, off), (fine_area, _))) in display.iter().zip(&fine).enumerate() { // A centroid sags at most about the chord tolerance (the bad sheet was 7 mm off at 0.6). assert!(*off < 2.0 * tol, "face {fi}: a triangle lies {off:.2} mm off the surface (tolerance {tol:.2})"); diff --git a/crates/kernel/src/tspline.rs b/crates/kernel/src/tspline.rs index 7688c72082..485ab575c7 100644 --- a/crates/kernel/src/tspline.rs +++ b/crates/kernel/src/tspline.rs @@ -191,7 +191,7 @@ mod tests { assert_eq!(shell.shell_condition(), mt::ShellCondition::Oriented, "neighbours share edges, run opposite ways"); let euler = body.vertex_count() as i64 - body.edge_count() as i64 + body.face_count() as i64; assert_eq!(euler, 1, "one sewn sheet (a disc)"); - let mesh = body.tessellate(0.01).unwrap(); + let mesh = body.mesh(&crate::MeshTolerance::chord(0.01)).unwrap(); assert!(!mesh.triangles.is_empty()); // Mesh points lie on the surface (not merely within the tessellation tolerance). let step = (mesh.positions.len() / 300).max(1); @@ -227,12 +227,12 @@ mod tests { for pts in cases { let Ok(ts) = TSpline::new(mesh.clone(), [3, 3], &pts, &vec![1.0; n]) else { continue }; if let Ok(body) = tspline_sheet(&ts) { - let _ = body.tessellate(0.01); + let _ = body.mesh(&crate::MeshTolerance::chord(0.01)); } if let Ok(r) = ts.refine(&[Cut::S { s: 0.33, t0: 0.3, t1: 0.6 }]) && let Ok(body) = tspline_sheet(&r) { - let _ = body.tessellate(0.01); + let _ = body.mesh(&crate::MeshTolerance::chord(0.01)); } } } diff --git a/crates/kernel/tests/chamfer_curved_holder.rs b/crates/kernel/tests/chamfer_curved_holder.rs index b1616a372e..1fc295d4af 100644 --- a/crates/kernel/tests/chamfer_curved_holder.rs +++ b/crates/kernel/tests/chamfer_curved_holder.rs @@ -59,7 +59,7 @@ fn chamfer_the_top_lip_at_a_tangent_wall_junction() { assert_eq!(made.crossed_face_loops().unwrap(), 0); let history = made.history().expect("the chamfer retains exact face provenance"); assert!(history.faces.iter().any(|f| f.origins.iter().any(|o| matches!(o, solidcraft_kernel::history::FaceOrigin::Edge { operand: 0, .. })))); - let mesh = |body: &Body| body.tessellate_with(0.0005, true).unwrap(); + let mesh = |body: &Body| body.mesh(&crate::MeshTolerance::chord(0.0005)).unwrap(); let before = mesh(&body).measure().volume; let after = mesh(&made); let removed = before - after.measure().volume; @@ -114,7 +114,7 @@ fn one_chamfer_pick_follows_the_tangent_chain_at_any_part_size() { assert!(made.validity().is_empty(), "{:?}", made.validity()); // Two straight edges (c^2/2 * 40 each) and both end arcs (Pappus, c^2/2 * 2 pi (R - c/3)). let expected = 40.0 + 0.5 * 2.0 * PI * (5.0 - 1.0 / 3.0); - let volume = |b: &Body| b.tessellate(1e-4).unwrap().measure().volume; + let volume = |b: &Body| b.mesh(&crate::MeshTolerance::chord(1e-4)).unwrap().measure().volume; let removed = volume(&body) - volume(&made); assert!((removed - expected).abs() < 0.3, "plate {plate}: removed {removed}, expected {expected}"); assert_eq!(made.face_count(), body.face_count() + 4, "plate {plate}: one chamfer face per rim edge"); diff --git a/crates/kernel/tests/revolve_on_axis.rs b/crates/kernel/tests/revolve_on_axis.rs index c9439d827e..ff7e8833ef 100644 --- a/crates/kernel/tests/revolve_on_axis.rs +++ b/crates/kernel/tests/revolve_on_axis.rs @@ -75,7 +75,7 @@ fn a_half_slot_revolved_about_a_centreline_in_a_face_cuts_it() { for turn in [PI, -PI, TAU] { let tool = revolve(&plane, std::slice::from_ref(&r), Vec2::ZERO, Vec2::new(1.0, 0.0), turn).unwrap().pop().unwrap(); // A half turn into the plate, or a full one, cuts a groove; a half turn out of it misses. - let into = turn.abs() > PI || tool.tessellate(0.01).unwrap().bounds().min.z < 9.0; + let into = turn.abs() > PI || tool.mesh(&crate::MeshTolerance::chord(0.01)).unwrap().bounds().min.z < 9.0; let cut = boolean(&plate, &tool, BoolOp::Cut).unwrap().unwrap(); assert!(cut.validity().is_empty(), "flip {flip}, {turn}: {:?}", cut.validity()); let removed = whole - measure(&cut).unwrap().volume; diff --git a/crates/kernel/tests/shell_barrel.rs b/crates/kernel/tests/shell_barrel.rs index 718d626f3c..fb933cbfbe 100644 --- a/crates/kernel/tests/shell_barrel.rs +++ b/crates/kernel/tests/shell_barrel.rs @@ -53,7 +53,7 @@ fn shell_of_a_revolved_barrel() { let result = shell(&barrel, &[Vec3::new(0.0, 0.0, 2.0 * HALF_HEIGHT)], 2.0).unwrap(); assert!(result.validity().is_empty(), "{:?}", result.validity()); assert_eq!(result.shell_count(), 1); - let mesh = result.tessellate(0.005).unwrap(); + let mesh = result.mesh(&crate::MeshTolerance::chord(0.005)).unwrap(); assert!(mesh.contains(Vec3::new(0.0, 0.0, 1.0)), "closed floor"); assert!(!mesh.contains(Vec3::new(0.0, 0.0, 3.0)), "open cavity"); assert!(mesh.contains(Vec3::new(13.0, 0.0, HALF_HEIGHT)), "barrel wall"); @@ -81,7 +81,7 @@ fn barrel_shell_opens_either_or_both_caps_and_refuses_a_vanished_cavity() { } let result = shell(&barrel, &opened, 2.0).unwrap(); assert!(result.validity().is_empty()); - let mesh = result.tessellate(0.005).unwrap(); + let mesh = result.mesh(&crate::MeshTolerance::chord(0.005)).unwrap(); assert!(!mesh.contains(Vec3::new(0.0, 0.0, 1.0))); assert_eq!(mesh.contains(Vec3::new(0.0, 0.0, 25.8)), !both); check_volume(&result, 0.0, 2.0 * HALF_HEIGHT - if both { 0.0 } else { 2.0 }); diff --git a/crates/ui-egui/src/dialogs_assembly.rs b/crates/ui-egui/src/dialogs_assembly.rs index 59ee719a45..d546981399 100644 --- a/crates/ui-egui/src/dialogs_assembly.rs +++ b/crates/ui-egui/src/dialogs_assembly.rs @@ -786,7 +786,7 @@ fn interference(app: &mut SolveApp, picked: &[Sel], clearance: &str) -> (Value, for hit in r["overlaps"].as_array().into_iter().flatten() { let (Some(a), Some(b)) = (hit["a"].as_str().and_then(|n| st.body(n)), hit["b"].as_str().and_then(|n| st.body(n))) else { continue }; if let Ok(Some(o)) = solidcraft_engine::kernel::boolean(&a.body, &b.body, solidcraft_engine::kernel::BoolOp::Intersect) - && let Ok(m) = o.tessellate((o.size() * 2e-3).max(1e-3)) + && let Ok(m) = o.mesh(&solidcraft_engine::kernel::MeshTolerance::chord((o.size() * 2e-3).max(1e-3))) { meshes.push(m); } diff --git a/crates/ui-egui/src/preselect_tests.rs b/crates/ui-egui/src/preselect_tests.rs index e261be25cc..5d9f87278d 100644 --- a/crates/ui-egui/src/preselect_tests.rs +++ b/crates/ui-egui/src/preselect_tests.rs @@ -315,7 +315,7 @@ fn cylinder_emboss_empty_target_face_applies_automatic_wrap_and_deboss() { for deboss in [false, true] { let mut h = crate::scenario::Harness::new(); h.app.run("solid.cylinder", json!({"radius":10,"height":20,"body_name":"Sleeve"})).unwrap(); - let initial = h.app.session.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume; + let initial = h.app.session.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume; let sketch = h.app.run("sketch.create", json!({"plane":{"origin":[0,10,0],"x_dir":[-1,0,0],"y_dir":[0,0,1]},"name":"Badge"})).unwrap()["sketch"] .as_u64() @@ -336,7 +336,7 @@ fn cylinder_emboss_empty_target_face_applies_automatic_wrap_and_deboss() { assert!(commands[0].1.get("face").is_none()); let preview = h.app.session.preview_world(&commands).unwrap(); let expected = if deboss { -19.0 } else { 21.0 }; - let preview_delta = preview.after.body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume - initial; + let preview_delta = preview.after.body("Sleeve").unwrap().body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume - initial; assert!( (preview_delta - expected).abs() < 0.05, "deboss={deboss}, preview delta={preview_delta}, expected={expected}, commands={commands:?}" @@ -350,7 +350,7 @@ fn cylinder_emboss_empty_target_face_applies_automatic_wrap_and_deboss() { assert_eq!(*actual, deboss); assert_eq!(face.body, "Sleeve"); assert!( - (h.app.session.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume - initial - expected) + (h.app.session.model.state().body("Sleeve").unwrap().body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume - initial - expected) .abs() < 0.05 ); @@ -2453,7 +2453,7 @@ fn point_along_path_has_its_position_and_missing_picks_are_named() { fn convert_mesh_asks_for_the_body_and_converts_the_selected_one() { use solidcraft_engine::doc::{Document, FeatureKind, MeshData}; let body = solidcraft_engine::kernel::box_solid(Vec3::ZERO, Vec3::new(10., 20., 30.)).unwrap(); - let mesh = body.tessellate(0.01).unwrap(); + let mesh = body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(0.01)).unwrap(); let mut doc = Document::default(); doc.add_feature( FeatureKind::MeshImport {