mesh: local sizes in boxes, balls and on surfaces, graded (#435)

RegionSize is the exact graded field the structured meshers query;
the tetrahedral refinement bakes the same local sizes into its graded
background grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 17:08:27 -04:00
1 parent e5ac6d35ce
commit 950d86c54e
3 files changed
+256 -6

No files matched your search

+64 -4
View File
@@ -13,7 +13,7 @@
//! analytic (von Neumann's alternating projections converge to the intersection for surfaces
//! meeting transversally); the result is kept only when it stays close to the polyline.
use crate::size::SizeField;
use crate::size::{LocalSize, Region, SizeField};
use crate::surface::BoundarySurface;
use super::measure::{self, P, add, cross, dot, norm, scale, sub};
@@ -842,9 +842,13 @@ pub const MAX_GRID_NODES: usize = 1 << 21;
impl GradedSize {
/// `samples`: points with their local size. `grading`: the largest change of size per unit
/// length.
pub fn new(global: f64, samples: &[(P, f64)], lo: P, hi: P, grading: f64) -> GradedSize {
///
/// `locals`: regions with their size ([`crate::size::LocalSize`]), seeded at every grid node
/// within a cell of the region (boxes and balls: inside too) at the local size plus the
/// grading times the distance, then graded with the rest.
pub fn new(global: f64, samples: &[(P, f64)], locals: &[LocalSize], lo: P, hi: P, grading: f64) -> GradedSize {
let diag = measure::dist(lo, hi).max(f64::MIN_POSITIVE);
let finest = samples.iter().map(|s| s.1).fold(global, f64::min).max(diag * 1e-4);
let finest = samples.iter().map(|s| s.1).chain(locals.iter().map(|l| l.size)).filter(|h| *h > 0.0).fold(global, f64::min).max(diag * 1e-4);
let mut cell = finest.max(diag / 64.0) * 0.5;
let dims_for = |c: f64| [0, 1, 2].map(|k| (((hi[k] - lo[k]) / c).ceil().max(1.0) as usize) + 2);
let mut dims = dims_for(cell);
@@ -879,6 +883,47 @@ impl GradedSize {
}
}
}
// Seed: the grid nodes near each local region.
let reach = cell * 3f64.sqrt();
let range = |a: f64, b: f64, k: usize| -> (usize, usize) {
let first = ((a - reach - lo[k]) / cell).ceil().max(0.0);
let last = ((b + reach - lo[k]) / cell).floor().min((dims[k] - 1) as f64);
if first.is_finite() && last.is_finite() && first <= last { (first as usize, last as usize) } else { (1, 0) }
};
let mut seed_near = |a: P, b: P, h: f64, distance: &dyn Fn(P) -> f64| {
let r = [0, 1, 2].map(|k| range(a[k], b[k], k));
for k in r[2].0..=r[2].1 {
for j in r[1].0..=r[1].1 {
for i in r[0].0..=r[0].1 {
let node = [lo[0] + i as f64 * cell, lo[1] + j as f64 * cell, lo[2] + k as f64 * cell];
let d = distance(node);
if d <= reach
&& let Some(s) = values.get_mut(idx(i, j, k))
{
*s = s.min(h + g * d);
}
}
}
}
};
for l in locals {
if !(l.size > 0.0 && l.size.is_finite() && l.size < global) {
continue;
}
match &l.region {
Region::Triangles(tris) => {
for t in tris {
let a = [0, 1, 2].map(|k| t[0][k].min(t[1][k]).min(t[2][k]));
let b = [0, 1, 2].map(|k| t[0][k].max(t[1][k]).max(t[2][k]));
seed_near(a, b, l.size, &|p| measure::dist(p, closest_on_triangle(p, t[0], t[1], t[2])));
}
}
region => {
let (a, b) = region.bounds();
seed_near(a, b, l.size, &|p| region.distance(p));
}
}
}
// Fast sweeping: eight sweep orders, each relaxing against the three axis neighbours
// behind the sweep direction (and diagonals through them over passes).
if g > 0.0 {
@@ -983,9 +1028,24 @@ mod tests {
assert_eq!(closest_on_segment([0.5, 2.0, 0.0], a, b), [0.5, 0.0, 0.0]);
}
#[test]
fn graded_size_takes_local_regions() {
let boxed = LocalSize { region: Region::Box { lo: [8.0; 3], hi: [12.0; 3] }, size: 2.0 };
let face = LocalSize { region: Region::Triangles(vec![[[0.0, 0.0, 20.0], [20.0, 0.0, 20.0], [0.0, 20.0, 20.0]]]), size: 1.0 };
let f = GradedSize::new(8.0, &[], &[boxed, face], [0.0; 3], [20.0, 20.0, 20.0], 0.5);
let inside = f.size([10.0, 10.0, 10.0]);
assert!((inside - 2.0).abs() < 0.3, "{inside}");
let on_face = f.size([5.0, 5.0, 20.0]);
assert!((on_face - 1.0).abs() < 0.3, "{on_face}");
// Four units off the box: about 2 + 0.5 × 4.
let off = f.size([10.0, 10.0, 4.0]);
assert!((off - 4.0).abs() < 0.6, "{off}");
assert_eq!(f.size([20.0, 20.0, 0.0]), 8.0);
}
#[test]
fn graded_size_grows_away_from_samples() {
let f = GradedSize::new(10.0, &[([0.0; 3], 1.0)], [0.0; 3], [20.0, 20.0, 20.0], 0.5);
let f = GradedSize::new(10.0, &[([0.0; 3], 1.0)], &[], [0.0; 3], [20.0, 20.0, 20.0], 0.5);
let near = f.size([0.0; 3]);
let mid = f.size([4.0, 0.0, 0.0]);
let far = f.size([20.0, 20.0, 20.0]);
+7 -2
View File
@@ -97,6 +97,9 @@ pub struct QualityOptions {
/// grows from it at the grading rate. A seed finer than the global size refines along its
/// edge; one coarser leaves it to the global size.
pub seeds: Vec<crate::seeds::EdgeSeed>,
/// Local sizes (size controls): the size field is at most each one's size in its region and
/// grows from it at the grading rate.
pub locals: Vec<crate::size::LocalSize>,
/// B-rep face tags whose boundary triangles are kept exactly: their vertices stay where they
/// are and no triangle of theirs is split, flipped or removed. The faces a pyramid
/// transition shares with the tetrahedra ([`crate::transition`]) are frozen, so the two
@@ -122,6 +125,7 @@ impl Default for QualityOptions {
max_tets: 5_000_000,
max_points: 1_000_000,
seeds: Vec::new(),
locals: Vec::new(),
frozen_faces: Vec::new(),
}
}
@@ -237,7 +241,8 @@ fn bounds(points: &[[f64; 3]]) -> ([f64; 3], [f64; 3]) {
(lo, hi)
}
/// The size field of a boundary: `global` limited by curvature, graded.
/// The size field of a boundary: `global` limited by curvature, thin walls, edge seeds and local
/// sizes, graded.
pub fn size_field(surface: &BoundarySurface, geometry: &Tessellation, global: f64, o: &QualityOptions) -> GradedSize {
let (lo, hi) = bounds(surface.points());
let floor = o.min_size * global;
@@ -268,7 +273,7 @@ pub fn size_field(surface: &BoundarySurface, geometry: &Tessellation, global: f6
}
}
}
GradedSize::new(global, &samples, lo, hi, o.grading)
GradedSize::new(global, &samples, &o.locals, lo, hi, o.grading)
}
/// Put the seeds' nodes on their feature curves, pinned (corner vertices, which neither