mesh: one transfinite interpolation for mapped hex and mesh.block; stable oracle report (#52)

* mesh: one transfinite interpolation for mapped hex and mesh.block; stable oracle report

mesh.block's lattice is now built from the structured hex mesher's
transfinite code: faces by quad::transfinite_grid, the interior by the new
quad::transfinite_volume (Gordon-Thiel, blended by the block's graded
parameters), which hex::mapped now calls too. 2D patches are the
transfinite_grid of their sides. Edge curves return their corners exactly.

Oracle: a zero-reference quantity (rounding noise) keeps its absolute
tolerance as its fixture band, and docs/oracle.md shows such noise and
relative errors inside the fixture band as bounds, so the report no longer
changes with the order of floating-point operations (it failed the
'docs/oracle.md is current' check on main after #50). Scordelis-Lo moved by
1.4e-9 relative (an ill-conditioned thin shell of solids: sub-ulp node
changes) and is re-blessed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* simplify: shared face filling and lattice index; faces blended like the volume; exact ends for every edge

quad::transfinite_faces fills a block's six faces (with a per-face hook
that hex::mapped uses to project onto its B-rep faces) and
quad::lattice_index fixes the lattice layout for all three users.
transfinite_grid_blended blends faces by the block's graded parameters, so
faces and interior use one parametrisation and mesh.block reproduces its
earlier Gordon-Hall lattice (the Scordelis-Lo fixture is back to its
blessed value). Straight edges also return their corners exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 22:50:20 -04:00
1 parent cd5e1e9df9
commit 85ba526663
16 files changed
+331 -239

No files matched your search

+36 -90
View File
@@ -12,10 +12,9 @@
use std::collections::BTreeMap;
use super::geom::{add, scale};
use super::interval::Intervals;
use super::{Ctx, Raw, project_to_face, project_to_faces};
use crate::quad::transfinite_grid;
use crate::quad::{lattice_index, transfinite_faces, transfinite_volume};
use crate::size::{divide_polyline, ideal_divisions};
use crate::vec3::{P, det, sub};
use crate::{Error, Result};
@@ -114,7 +113,7 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
let last = pts.len().saturating_sub(1);
Ok(pts.iter().enumerate().map(|(k, q)| if k == 0 || k == last { *q } else { project_to_faces(ctx, &faces, *q) }).collect())
};
let idx = |i: usize, j: usize, k: usize| (k * (nj + 1) + j) * (ni + 1) + i;
let idx = |i: usize, j: usize, k: usize| lattice_index([ni, nj, nk], [i, j, k]);
let mut x: Vec<Option<P>> = vec![None; total];
// Edges: E[d][s][t] along direction d at the other two coordinates (s, t) ∈ {0, 1}².
for (d, g) in groups.iter().enumerate() {
@@ -133,96 +132,43 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
}
}
}
let get = |x: &[Option<P>], i: usize, j: usize, k: usize| -> Result<P> {
x.get(idx(i, j, k)).copied().flatten().ok_or_else(|| Error::Failed("mapped grid node".into()))
};
// Faces: grids from their edges, projected onto the face found at the face's centre.
for d in 0..3 {
for side in 0..2 {
// Face d = side·n_d; its parameters (a, b) run along the other two directions.
let (da, db) = match d {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
};
let (na, nb) = ([ni, nj, nk][da], [ni, nj, nk][db]);
let ijk = |a: usize, b: usize| -> (usize, usize, usize) {
let mut c = [0usize; 3];
c[d] = side * [ni, nj, nk][d];
c[da] = a;
c[db] = b;
(c[0], c[1], c[2])
};
let line = |f: &dyn Fn(usize) -> (usize, usize, usize), n: usize| -> Result<Vec<P>> {
(0..=n)
.map(|q| {
let (i, j, k) = f(q);
get(&x, i, j, k)
})
.collect()
};
let bottom = line(&|a| ijk(a, 0), na)?;
let top = line(&|a| ijk(a, nb), na)?;
let left = line(&|b| ijk(0, b), nb)?;
let right = line(&|b| ijk(na, b), nb)?;
let grid = transfinite_grid(&bottom, &right, &top, &left)?;
// The B-rep face bounded by this side's four edges.
let logical = |a: usize, b: usize| {
let (i, j, k) = ijk(a, b);
at(usize::from(i > 0), usize::from(j > 0), usize::from(k > 0))
};
let ring = [logical(0, 0), logical(na, 0), logical(na, nb), logical(0, nb)];
let mut bounding: Vec<usize> = (0..4).filter_map(|m| edge_between(ring[m], ring[(m + 1) % 4]).map(|x| x.0)).collect();
bounding.sort_unstable();
let face = brep.faces.iter().position(|f| {
let mut es: Vec<usize> = f.loops.iter().flatten().map(|x| x.0).collect();
es.sort_unstable();
es == bounding
});
for b in 1..nb {
for a in 1..na {
let q = grid.get(b * (na + 1) + a).copied().unwrap_or([0.0; 3]);
let q = face.and_then(|f| project_to_face(ctx, f, q)).unwrap_or(q);
let (i, j, k) = ijk(a, b);
if let Some(slot) = x.get_mut(idx(i, j, k)) {
*slot = Some(q);
}
// Faces: grids from their edges, projected onto the B-rep face their four edges bound.
let n = [ni, nj, nk];
transfinite_faces(&mut x, n, None, |d, side, grid| {
let (da, db) = match d {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
};
let logical = |a: usize, b: usize| {
let mut c = [0usize; 3];
c[d] = side;
c[da] = a;
c[db] = b;
at(c[0], c[1], c[2])
};
let ring = [logical(0, 0), logical(1, 0), logical(1, 1), logical(0, 1)];
let mut bounding: Vec<usize> = (0..4).filter_map(|m| edge_between(ring[m], ring[(m + 1) % 4]).map(|x| x.0)).collect();
bounding.sort_unstable();
let face = brep.faces.iter().position(|f| {
let mut es: Vec<usize> = f.loops.iter().flatten().map(|x| x.0).collect();
es.sort_unstable();
es == bounding
});
let (na, nb) = (n[da], n[db]);
for b in 1..nb {
for a in 1..na {
if let Some(q) = grid.get_mut(b * (na + 1) + a) {
*q = face.and_then(|f| project_to_face(ctx, f, *q)).unwrap_or(*q);
}
}
}
}
// Volume: trivariate transfinite interpolation.
let (fi, fj, fk) = (ni as f64, nj as f64, nk as f64);
for k in 1..nk {
for j in 1..nj {
for i in 1..ni {
let (u, v, w) = (i as f64 / fi, j as f64 / fj, k as f64 / fk);
let g = |i: usize, j: usize, k: usize| get(&x, i, j, k);
let lin = |t: f64, a: P, b: P| add(scale(a, 1.0 - t), scale(b, t));
// Faces.
let faces = add(add(lin(u, g(0, j, k)?, g(ni, j, k)?), lin(v, g(i, 0, k)?, g(i, nj, k)?)), lin(w, g(i, j, 0)?, g(i, j, nk)?));
// Edges.
let bil = |s: f64, t: f64, p00: P, p10: P, p01: P, p11: P| lin(t, lin(s, p00, p10), lin(s, p01, p11));
let edges = add(
add(
bil(v, w, g(i, 0, 0)?, g(i, nj, 0)?, g(i, 0, nk)?, g(i, nj, nk)?),
bil(u, w, g(0, j, 0)?, g(ni, j, 0)?, g(0, j, nk)?, g(ni, j, nk)?),
),
bil(u, v, g(0, 0, k)?, g(ni, 0, k)?, g(0, nj, k)?, g(ni, nj, k)?),
);
// Corners.
let corners = lin(
w,
bil(u, v, g(0, 0, 0)?, g(ni, 0, 0)?, g(0, nj, 0)?, g(ni, nj, 0)?),
bil(u, v, g(0, 0, nk)?, g(ni, 0, nk)?, g(0, nj, nk)?, g(ni, nj, nk)?),
);
let p = add(sub(faces, edges), corners);
if let Some(slot) = x.get_mut(idx(i, j, k)) {
*slot = Some(p);
}
}
}
}
Ok(())
})?;
// Volume: trivariate transfinite interpolation, blended by index.
let uniform = |n: usize| -> Vec<f64> { (0..=n).map(|q| q as f64 / n as f64).collect() };
let (bi, bj, bk) = (uniform(ni), uniform(nj), uniform(nk));
transfinite_volume(&mut x, [ni, nj, nk], [&bi, &bj, &bk])?;
let nodes: Vec<P> = x.into_iter().map(|p| p.ok_or_else(|| Error::Failed("mapped grid node missing".into()))).collect::<Result<_>>()?;
let id = |i: usize, j: usize, k: usize| idx(i, j, k) as u32;
let mut hexes = Vec::with_capacity(ni * nj * nk);