mesh: block projection divides its edges by a graded size field (#435)

ProjectOptions::field measures each block edge in units of the field,
so the division counts follow the size controls.

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 4d0790565c
commit c611080182
1 file changed
+21 -8
+21 -8
View File
@@ -72,8 +72,9 @@ const MAX_STRETCH: f64 = 2.0;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct ProjectOptions { pub struct ProjectOptions {
/// The element size: each class of parallel block edges gets `round(length / size)` /// The element size: each class of parallel block edges gets `round(length / size)`
/// divisions (at least one), `length` the geometric mean of its projected edges' lengths, /// divisions (at least one), `length / size` the geometric mean of its projected edges'
/// and enough that no element is longer than twice the size. /// lengths in sizes (measured by `field` when there is one), and enough that no element is
/// longer than twice the size.
pub size: f64, pub size: f64,
/// The largest angle (radians) the surface may turn through across one element on a curved /// The largest angle (radians) the surface may turn through across one element on a curved
/// target face: a class takes enough divisions that its elements are no longer than the /// target face: a class takes enough divisions that its elements are no longer than the
@@ -98,6 +99,10 @@ pub struct ProjectOptions {
/// corners lie on one (within a tenth of the size) keeps its nodes on it through smoothing, so /// corners lie on one (within a tenth of the size) keeps its nodes on it through smoothing, so
/// the mesh can be cut there into two meshes with planar faces. /// the mesh can be cut there into two meshes with planar faces.
pub planes: Vec<(P, P)>, pub planes: Vec<(P, P)>,
/// A graded size field (size controls) in place of the uniform `size` when the divisions are
/// chosen: an edge's length is then measured in units of the field ([`crate::size::ideal_divisions`]).
/// Its global size is `size`.
pub field: Option<crate::size::RegionSize>,
} }
impl Default for ProjectOptions { impl Default for ProjectOptions {
@@ -113,6 +118,7 @@ impl Default for ProjectOptions {
min_quality: DEFAULT_MIN_QUALITY, min_quality: DEFAULT_MIN_QUALITY,
fixed: Vec::new(), fixed: Vec::new(),
planes: Vec::new(), planes: Vec::new(),
field: None,
} }
} }
} }
@@ -511,6 +517,7 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
// 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.
let (classes, nclass) = edge_classes(t); let (classes, nclass) = edge_classes(t);
let mut class_log = vec![(0.0f64, 0usize); nclass]; 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_max = vec![0.0f64; nclass];
let mut seen: BTreeSet<[u32; 2]> = BTreeSet::new(); let mut seen: BTreeSet<[u32; 2]> = BTreeSet::new();
for (block, cls) in t.blocks.iter().zip(&classes) { for (block, cls) in t.blocks.iter().zip(&classes) {
@@ -528,17 +535,23 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
let (pa, pb) = (vpos.get(a as usize).copied().unwrap_or_default(), vpos.get(b as usize).copied().unwrap_or_default()); let (pa, pb) = (vpos.get(a as usize).copied().unwrap_or_default(), vpos.get(b as usize).copied().unwrap_or_default());
let c = edge_c(edge_key(a, b)); let c = edge_c(edge_key(a, b));
let mut len = 0.0; let mut len = 0.0;
let mut prev = pa; let mut line = Vec::with_capacity(17);
line.push(pa);
for q in 1..=16 { for q in 1..=16 {
let x = if q == 16 { pb } else { place(g, &targets, c, lerp(pa, pb, q as f64 / 16.0), tol) }; let x = if q == 16 { pb } else { place(g, &targets, c, lerp(pa, pb, q as f64 / 16.0), tol) };
len += dist(prev, x); len += dist(*line.last().unwrap_or(&pa), x);
prev = x; line.push(x);
} }
// The edge's length in element sizes.
let measured = match &opts.field {
Some(f) if len > tol => crate::size::ideal_divisions(f, &line)?,
_ => len / opts.size,
};
if let Some(m) = class_max.get_mut(cls[d]) { if let Some(m) = class_max.get_mut(cls[d]) {
*m = m.max(len); *m = m.max(measured);
} }
if let Some(l) = class_log.get_mut(cls[d]).filter(|_| len > tol) { if let Some(l) = class_log.get_mut(cls[d]).filter(|_| len > tol) {
l.0 += (len / opts.size).ln(); l.0 += measured.ln();
l.1 += 1; l.1 += 1;
} }
} }
@@ -604,7 +617,7 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
.zip(&class_arc) .zip(&class_arc)
.map(|((&(sum, k), &longest), &arc)| { .map(|((&(sum, k), &longest), &arc)| {
let mean = if k == 0 { 1.0 } else { (sum / k as f64).exp().round() }; let mean = if k == 0 { 1.0 } else { (sum / k as f64).exp().round() };
(mean.max((longest / (MAX_STRETCH * opts.size)).ceil()) as usize).max(arc).clamp(1, crate::block::MAX_DIVISIONS) (mean.max((longest / MAX_STRETCH).ceil()) as usize).max(arc).clamp(1, crate::block::MAX_DIVISIONS)
}) })
.collect(); .collect();
for &(b, d, n) in &opts.divisions { for &(b, d, n) in &opts.divisions {