diff --git a/crates/engine/src/cmd/project/tests.rs b/crates/engine/src/cmd/project/tests.rs index 3bc3205..08eae1c 100644 --- a/crates/engine/src/cmd/project/tests.rs +++ b/crates/engine/src/cmd/project/tests.rs @@ -202,8 +202,12 @@ fn squeezed_junction_elements_are_reported_not_meshed() { p["min_quality"] = json!(0.3); let model = s.model.clone(); let e = s.execute("mesh.hex.project", &p).unwrap_err().to_string(); - // Smoothing leaves the worst element in the squeezed ring block (34) or in block 36. - assert!(e.contains("smallest scaled Jacobian") && (e.contains("worst in block 34") || e.contains("worst in block 36")), "{e}"); + // Smoothing leaves the worst element in the squeezed ring block (34) or in block 36; with + // the cup's rim divided by its own length, the junction's corner (block 13) folds instead, + // and the validity gate refuses it. + let squeezed = e.contains("smallest scaled Jacobian") && (e.contains("worst in block 34") || e.contains("worst in block 36")); + let folded = e.contains("inverted, twisted or degenerate") && e.contains("block 13"); + assert!(squeezed || folded, "{e}"); assert_eq!(s.model, model); } diff --git a/crates/mesh/src/project/mod.rs b/crates/mesh/src/project/mod.rs index cc79340..30d3faa 100644 --- a/crates/mesh/src/project/mod.rs +++ b/crates/mesh/src/project/mod.rs @@ -514,11 +514,17 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o // lengths). A class runs through every block its edges are parallel in, so its edges can // differ a lot in length (a cup's wall, 4 mm thick, and its floor's radial edges, twice // that); the longest edge alone would cut the thin blocks into flat slivers. No element is - // longer than [`MAX_STRETCH`] sizes, so a coarse size still follows the geometry. + // longer than [`MAX_STRETCH`] sizes, so a coarse size still follows the geometry. An edge + // that bends with a curved target is the exception: its elements' sides are chords of the + // surface, and one longer than the size cuts off more of the part than the size allows (a + // butterfly's rim shares its class with the shorter sides of its middle square), so a class + // takes at least the geometric mean of its bent edges' lengths in sizes (rounded, + // as the mean; an annulus's rim and bore balance each other as its straight edges do). let (classes, nclass) = edge_classes(t); let mut class_log = vec![(0.0f64, 0usize); nclass]; // Per class, its longest edge in element sizes. let mut class_max = vec![0.0f64; nclass]; + let mut class_bent = vec![(0.0f64, 0usize); nclass]; let mut seen: BTreeSet<[u32; 2]> = BTreeSet::new(); for (block, cls) in t.blocks.iter().zip(&classes) { for d in 0..3 { @@ -550,6 +556,12 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o if let Some(m) = class_max.get_mut(cls[d]) { *m = m.max(measured); } + if len - dist(pa, pb) > tol + && let Some(l) = class_bent.get_mut(cls[d]) + { + l.0 += libm::log(measured); + l.1 += 1; + } if let Some(l) = class_log.get_mut(cls[d]).filter(|_| len > tol) { l.0 += libm::log(measured); l.1 += 1; @@ -615,9 +627,11 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o .iter() .zip(&class_max) .zip(&class_arc) - .map(|((&(sum, k), &longest), &arc)| { + .zip(&class_bent) + .map(|(((&(sum, k), &longest), &arc), &(bent_sum, bent_k))| { let mean = if k == 0 { 1.0 } else { libm::exp(sum / k as f64).round() }; - (mean.max((longest / MAX_STRETCH).ceil()) as usize).max(arc).clamp(1, crate::block::MAX_DIVISIONS) + let bent = if bent_k == 0 { 0 } else { libm::exp(bent_sum / bent_k as f64).round().min(crate::block::MAX_DIVISIONS as f64) as usize }; + (mean.max((longest / MAX_STRETCH).ceil()) as usize).max(arc).max(bent).clamp(1, crate::block::MAX_DIVISIONS) }) .collect(); for &(b, d, n) in &opts.divisions {