diff --git a/crates/mesh/src/octree/mod.rs b/crates/mesh/src/octree/mod.rs index bd3a7aa..05be08d 100644 --- a/crates/mesh/src/octree/mod.rs +++ b/crates/mesh/src/octree/mod.rs @@ -236,7 +236,8 @@ enum Attempt { } /// Mesh the solid bounded by `surface` with hexahedra of about `field`'s size (the root cell -/// size is the field at the centre of the part's box; finer cells follow the part's curvature, +/// size is the field's largest size ([`SizeField::range`]), else the field at the centre of the +/// part's box; cells are refined where the field is smaller than they are; finer cells follow the part's curvature, /// thickness and narrow faces). See the module docs. pub fn octree_hex_mesh(surface: &BoundarySurface, field: &dyn SizeField, options: &OctreeOptions) -> Result { check_options(options)?; @@ -323,7 +324,26 @@ fn plan<'a>(g: &'a Geometry<'a>, field: &'a dyn SizeField, options: &'a OctreeOp let frame = choose_frame(g); let (lo, hi) = frame_box(g, &frame); let centre = from_frame(&frame, [0, 1, 2].map(|k| 0.5 * (lo[k] + hi[k]))); - let h = checked_size(field, centre)?; + // The root cells at the largest size; the depth reaches the smallest (size controls). + let h = match field.range() { + Some((_, coarsest)) if coarsest > 0.0 && coarsest.is_finite() => coarsest, + _ => checked_size(field, centre)?, + }; + if let Some((finest, _)) = field.range() + && finest > 0.0 + && finest < h + { + match levels_below(h, finest) { + Some(d) if d <= options.max_depth => {} + d => { + let n = d.map_or_else(|| format!("more than {}", grid::MAX_LEVEL), |l| l.to_string()); + return Err(Error::Limit(format!( + "a size of {finest:.4} below the root size {h:.4} needs {n} levels (each a third); `max_depth` is {}: raise it, or use a larger local size", + options.max_depth + ))); + } + } + } let needs = Needs::of(g, thickness, options); let deep = levels_below(h, needs.edge); let mut anchors: Vec<(f64, Option)> = vec![(h, deep)]; diff --git a/crates/mesh/src/octree/tests.rs b/crates/mesh/src/octree/tests.rs index ffaac1a..c500a04 100644 --- a/crates/mesh/src/octree/tests.rs +++ b/crates/mesh/src/octree/tests.rs @@ -159,3 +159,26 @@ fn meshing_is_deterministic() { let b = octree_hex_mesh(&s, &UniformSize(2.0), &OctreeOptions::default()).unwrap(); assert_eq!(a, b); } + +/// A size box refines the grid inside it to the level its size needs (closure may carry that +/// level further), and the root cells keep the global size; a size deeper than `max_depth` allows is refused before meshing. +#[test] +fn a_size_box_refines_the_grid_inside_it() { + use crate::size::{LocalSize, Region, RegionSize}; + let s = box_surface([0.0; 3], [18.0, 18.0, 18.0]); + let local = LocalSize { region: Region::Box { lo: [0.0; 3], hi: [6.0; 3] }, size: 2.0 }; + let field = RegionSize::new(6.0, 0.3, vec![local.clone()]).unwrap(); + let m = octree_hex_mesh(&s, &field, &OctreeOptions::default()).unwrap(); + let edge = |h: &[u32; 8]| { + let (a, b) = (m.nodes[h[0] as usize], m.nodes[h[1] as usize]); + ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt() + }; + let centre = |h: &[u32; 8]| [0, 1, 2].map(|k| h.iter().map(|&v| m.nodes[v as usize][k]).sum::() / 8.0); + let inside: Vec = m.hexes.iter().filter(|h| centre(h).iter().all(|&c| (1.0..5.0).contains(&c))).map(edge).collect(); + assert!(!inside.is_empty() && inside.iter().all(|&e| e < 2.0 * 1.2), "{inside:?}"); + assert_eq!((m.report.root_size, m.report.levels), (6.0, 1), "{:?}", m.report); + let shallow = OctreeOptions { max_depth: 1, curvature_levels: 1, ..OctreeOptions::default() }; + let deep = RegionSize::new(6.0, 0.3, vec![LocalSize { size: 0.5, ..local }]).unwrap(); + let e = octree_hex_mesh(&s, &deep, &shallow).unwrap_err().to_string(); + assert!(e.contains("max_depth"), "{e}"); +} diff --git a/crates/mesh/src/size.rs b/crates/mesh/src/size.rs index 907e35d..47da0f5 100644 --- a/crates/mesh/src/size.rs +++ b/crates/mesh/src/size.rs @@ -29,6 +29,13 @@ use crate::{Error, Result}; /// treat anything else as invalid input. pub trait SizeField { fn size(&self, p: [f64; 3]) -> f64; + + /// The smallest and largest size anywhere, when the field knows them (a mesher that builds + /// from a root size, such as the octree, starts from the largest and plans its depth from + /// the smallest). + fn range(&self) -> Option<(f64, f64)> { + None + } } impl f64> SizeField for F { @@ -45,6 +52,10 @@ impl SizeField for UniformSize { fn size(&self, _: [f64; 3]) -> f64 { self.0 } + + fn range(&self) -> Option<(f64, f64)> { + Some((self.0, self.0)) + } } /// The largest division count of one curve. @@ -348,6 +359,10 @@ impl SizeField for RegionSize { } h } + + fn range(&self) -> Option<(f64, f64)> { + Some((self.finest(), self.global)) + } } #[cfg(test)]