diff --git a/crates/engine/src/sample.rs b/crates/engine/src/sample.rs index 8353a58..7ef03ca 100644 --- a/crates/engine/src/sample.rs +++ b/crates/engine/src/sample.rs @@ -821,6 +821,38 @@ mod tests { } } + /// The one-to-one sweep and the crank (#444). The arm's profile extruded alone is a + /// one-to-one sweep (one source, one target) and sweeps all-hex as well as extrude does. The + /// crank itself ends at several levels (pocket floor at 8 mm, arm 16, pedal boss 20, spindle + /// boss 22), so no one-to-one sweep takes it: the sweep says so, naming a cap only part of a + /// ring reaches, where it reported a "linking face with no top side"; extrude meshes it. + #[test] + fn the_sweep_takes_the_arm_profile_and_names_the_crank_levels() { + let session = |script: &str| { + let bytes = fieldcraft_geometry::script_design(script).unwrap(); + let (dir, path) = crate::tests::design_file(&bytes); + let mut s = Session::default(); + s.execute("geometry.open", &json!({"path": path})).unwrap(); + (dir, s) + }; + let quality = |s: &mut Session, method: &str| -> crate::Result { + let r = s.execute("mesh.hex.generate", &json!({"body": "CrankArm", "size": 3, "method": method}))?; + assert_eq!(r["bodies"][0]["method"], method, "{r}"); + Ok(r["bodies"][0]["quality"]["min_scaled_jacobian"].as_f64().unwrap()) + }; + // The script up to the arm's extrusion: the profile alone. + let end = CRANK_ARM.find("{\"command\": \"solid.cylinder\"").unwrap(); + let profile = format!("{}]}}", CRANK_ARM[..end].trim_end().trim_end_matches(',')); + let (_dir, mut s) = session(&profile); + let (sweep, extrude) = (quality(&mut s, "sweep").unwrap(), quality(&mut s, "extrude").unwrap()); + assert!(sweep > 0.3 && sweep > 0.95 * extrude, "sweep {sweep}, extrude {extrude}"); + + let (_dir, mut s) = session(CRANK_ARM); + let e = quality(&mut s, "sweep").unwrap_err().to_string(); + assert!(e.contains("is parallel to the source and only part of the ring reaches it") && e.contains("method `extrude`"), "{e}"); + assert!(quality(&mut s, "extrude").unwrap() > 0.35); + } + /// `file.sample NAME`, checked clean: ready, no errors or warnings. fn open(name: &str) -> Session { let mut s = Session::default(); diff --git a/crates/mesh/src/hex/sweep.rs b/crates/mesh/src/hex/sweep.rs index 0800c76..ba40e05 100644 --- a/crates/mesh/src/hex/sweep.rs +++ b/crates/mesh/src/hex/sweep.rs @@ -33,7 +33,7 @@ use super::region; use super::{Ctx, Raw, project_to_face, project_to_faces}; use crate::quad::transfinite_grid; use crate::size::ideal_divisions; -use crate::vec3::{P, sub}; +use crate::vec3::{P, dot, sub}; use crate::{Error, Result}; /// The most source faces tried. @@ -118,7 +118,10 @@ fn topology(brep: &Brep, s: usize) -> std::result::Result { run.push((e, fwd)); k += 1; } - ring.push(link(brep, f, &run)?); + ring.push(link(brep, f, &run).map_err(|e| match cap(brep, s, f) { + Some(why) => why, + None => e, + })?); } used.extend(ring.iter().map(|l| l.face)); front = ring.iter().flat_map(|l| l.top.iter().map(move |&(e, fwd)| (e, fwd, l.face))).collect(); @@ -140,6 +143,21 @@ fn topology(brep: &Brep, s: usize) -> std::result::Result { Ok(Topology { source: s, target, rings }) } +/// Why face `f`, reached from a front swept from source `s`, cannot be a linking face when it is +/// a plane parallel to the source: a cap that only part of the ring reaches. The body then ends +/// at several levels (a step, a boss, a pocket floor), which no one-to-one sweep takes; saying so +/// names the reason where the linking-face split would only report the symptom (#444). +fn cap(brep: &Brep, s: usize, f: usize) -> Option { + let (_, ns) = brep.faces.get(s)?.plane?; + let (_, nf) = brep.faces.get(f)?.plane?; + (dot(ns, nf).abs() > 1.0 - 1e-9).then(|| { + format!( + "face {} is parallel to the source and only part of the ring reaches it: the body ends at more than one level (a step, a boss or a pocket), and a one-to-one sweep has one target face; method `extrude` meshes such bodies level by level", + brep.face_tag(f) + ) + }) +} + /// Whether the front's edges are exactly one loop of face `t`. fn is_loop_of(brep: &Brep, t: usize, front: &[(usize, bool, usize)]) -> bool { let edges: BTreeSet = front.iter().map(|x| x.0).collect();