forked from 1b-agent/fieldcraft-upstream
mesh: carry a biased seed's spacing to the edges tied to it (#257)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
d23c2efaec
commit
3385209c6f
9 files changed
+208
-10
No files matched your search
@@ -327,12 +327,17 @@ impl Topology {
|
||||
ctxs.get(c).and_then(|x| x.seed_of(be)).map(|(seed, rev)| (seed, rev == fwd))
|
||||
})
|
||||
.collect();
|
||||
// A biased seed's spacing carries to the unseeded edges parallel to it, through every
|
||||
// cell they bound, oriented along each cell's direction (#257); the counts come from the
|
||||
// edges' own seeds alone.
|
||||
let carried = crate::seeds::carry(&seeds, |s: &&EdgeSeed| s.bias.is_some(), &self.parallel_ties());
|
||||
let first = self.solve(&goals, &seeds)?;
|
||||
// Ends of edges that leave a curved face: the spacing along that face there.
|
||||
let ends = self.graded_ends(ctxs, cubes, &first);
|
||||
let mut graded = goals;
|
||||
let mut divisions = Vec::with_capacity(polylines.len());
|
||||
for (((poly, ends), goal), seed) in polylines.into_iter().zip(ends).zip(graded.iter_mut()).zip(&seeds) {
|
||||
let spacing: Vec<Option<(&EdgeSeed, bool)>> = seeds.iter().zip(carried).map(|(own, carried)| own.or(carried)).collect();
|
||||
for (((poly, ends), goal), seed) in polylines.into_iter().zip(ends).zip(graded.iter_mut()).zip(&spacing) {
|
||||
// A seeded edge keeps the seed's spacing, not a grading.
|
||||
let ends = if seed.is_some() { [None, None] } else { ends };
|
||||
let poly = if ends.iter().any(Option::is_some) {
|
||||
@@ -350,6 +355,28 @@ impl Topology {
|
||||
Ok(divisions)
|
||||
}
|
||||
|
||||
/// The four edges along each direction of every cell, tied pairwise, each traversed from the
|
||||
/// cell's low corner to its high one.
|
||||
fn parallel_ties(&self) -> Vec<[crate::seeds::Use; 2]> {
|
||||
let mut ties = Vec::new();
|
||||
for (groups, ids) in self.cell_edges.iter().zip(&self.corners) {
|
||||
for (d, g) in groups.iter().enumerate() {
|
||||
let uses: Vec<crate::seeds::Use> = g
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(m, &e)| {
|
||||
let (lo, _) = edge_ends(ids, d, m);
|
||||
(e, self.edges.get(e).is_some_and(|x| x.a == lo))
|
||||
})
|
||||
.collect();
|
||||
if let Some((&first, rest)) = uses.split_first() {
|
||||
ties.extend(rest.iter().map(|&u| [first, u]));
|
||||
}
|
||||
}
|
||||
}
|
||||
ties
|
||||
}
|
||||
|
||||
fn solve(&self, goals: &[f64], seeds: &[Option<(&EdgeSeed, bool)>]) -> Result<Vec<usize>> {
|
||||
let mut ia = super::interval::Intervals::default();
|
||||
for (&g, seed) in goals.iter().zip(seeds) {
|
||||
|
||||
@@ -37,7 +37,7 @@ use super::brep::{Brep, Face};
|
||||
use super::decompose;
|
||||
use super::geom::{Inside, Surface, add, dist, scale, unit};
|
||||
use super::interval::Intervals;
|
||||
use super::region::Scheme;
|
||||
use super::region::{self, Scheme};
|
||||
use super::section;
|
||||
use super::{Ctx, Raw};
|
||||
use crate::predicates::Sign;
|
||||
@@ -453,6 +453,17 @@ pub(crate) fn mesh(ctx: &Ctx, prepared: &Prepared, refine: u32) -> Result<Raw> {
|
||||
}
|
||||
}
|
||||
let counts = ia.solve()?;
|
||||
// A biased seed's spacing carries across the cross-section's mapped regions to the opposite
|
||||
// curves without seeds of their own (#257).
|
||||
let ties: Vec<[crate::seeds::Use; 2]> = arr
|
||||
.regions
|
||||
.iter()
|
||||
.zip(&schemes)
|
||||
.filter_map(|(region, &scheme)| region.loops.first().map(|lp| region::opposite_ties(lp, scheme)))
|
||||
.flatten()
|
||||
.collect();
|
||||
let carried = crate::seeds::carry(&seeds.curves, |s: &&EdgeSeed| s.bias.is_some(), &ties);
|
||||
let spacing: Vec<Option<(&EdgeSeed, bool)>> = seeds.curves.iter().zip(carried).map(|(own, carried)| own.or(carried)).collect();
|
||||
let r_max = arr.points.iter().map(|q| q[1].abs()).fold(0.0, f64::max);
|
||||
let mut layers = Vec::with_capacity(intervals.len());
|
||||
for &(lo, hi) in intervals.iter() {
|
||||
@@ -475,7 +486,7 @@ pub(crate) fn mesh(ctx: &Ctx, prepared: &Prepared, refine: u32) -> Result<Raw> {
|
||||
}
|
||||
// The cross-section's nodes (curve interiors projected onto analytic walls) and quadrilaterals.
|
||||
let place = |c: usize, n: usize| -> Result<Vec<Q>> {
|
||||
let pts3 = match seeds.curves.get(c).copied().flatten() {
|
||||
let pts3 = match spacing.get(c).copied().flatten() {
|
||||
Some((seed, rev)) => crate::seeds::place(&curve3d(c), &seed.fractions(n, rev))?,
|
||||
None => divide_polyline(ctx.field, &curve3d(c), n)?,
|
||||
};
|
||||
|
||||
@@ -171,9 +171,13 @@ pub(crate) fn mesh(ctx: &Ctx) -> Result<Raw> {
|
||||
if ni * nj * nk > ctx.opts.max_elements {
|
||||
return Err(Error::Limit(format!("the mesh would have {} hexahedra; the limit is {}", ni * nj * nk, ctx.opts.max_elements)));
|
||||
}
|
||||
// A biased seed's spacing carries to the unseeded edges parallel to it: the four edges of a
|
||||
// direction, all low corner to high.
|
||||
let ties: Vec<[(usize, bool); 2]> = groups.iter().flat_map(|g| g.iter().skip(1).map(move |&u| [g[0], u])).collect();
|
||||
let carried = ctx.carried(&ties);
|
||||
// Edge node positions, low corner to high.
|
||||
let edge_pts = |(e, fwd): (usize, bool), n: usize| -> Result<Vec<P>> {
|
||||
let pts = ctx.divide_edge(e, fwd, n)?;
|
||||
let pts = ctx.divide_edge(e, fwd, n, &carried)?;
|
||||
let faces = brep.edges.get(e).map(|x| x.faces.to_vec()).unwrap_or_default();
|
||||
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())
|
||||
|
||||
@@ -219,14 +219,24 @@ impl<'a> Ctx<'a> {
|
||||
}
|
||||
|
||||
/// `n + 1` points along B-rep edge `e` (traversed forwards or not): at the seed's spacing
|
||||
/// when it has one, else evenly in the size field.
|
||||
pub(crate) fn divide_edge(&self, e: usize, forward: bool, n: usize) -> Result<Vec<P>> {
|
||||
/// when it has one, or the spacing `carried` to it from a biased seed on an edge it is tied
|
||||
/// to ([`Ctx::carried`]), else evenly in the size field.
|
||||
pub(crate) fn divide_edge(&self, e: usize, forward: bool, n: usize, carried: &[Option<(usize, bool)>]) -> Result<Vec<P>> {
|
||||
let pts = self.brep.edge_points(e, forward);
|
||||
match self.seed_of(e) {
|
||||
let carried = carried.get(e).copied().flatten().and_then(|(i, rev)| Some((self.opts.seeds.get(i)?, rev)));
|
||||
match self.seed_of(e).or(carried) {
|
||||
Some((seed, rev)) => crate::seeds::place(&pts, &seed.fractions(n, rev == forward)),
|
||||
None => crate::size::divide_polyline(self.field, &pts, n),
|
||||
}
|
||||
}
|
||||
|
||||
/// The seeds carried along `ties` (uses of B-rep edges divided alike) from biased seeds to
|
||||
/// unseeded edges ([`crate::seeds::carry`]), by index into the seeds.
|
||||
pub(crate) fn carried(&self, ties: &[[crate::seeds::Use; 2]]) -> Vec<Option<(usize, bool)>> {
|
||||
let mut own = self.edge_seeds.clone();
|
||||
own.resize(self.brep.edges.len(), None);
|
||||
crate::seeds::carry(&own, |&i| self.opts.seeds.get(i).is_some_and(|s| s.bias.is_some()), ties)
|
||||
}
|
||||
}
|
||||
|
||||
/// How far apart a seed's polyline and a mesher's curve of the same edge may lie: both are
|
||||
|
||||
@@ -109,13 +109,28 @@ pub(crate) fn classify(loops: &[Vec<Vec<Q>>]) -> Scheme {
|
||||
|
||||
/// The curve variables of each side of a loop between its corners (positions in increasing
|
||||
/// order); the last side wraps round to the first corner.
|
||||
fn loop_sides<const N: usize>(lp: &[usize], c: [usize; N]) -> [Vec<usize>; N] {
|
||||
fn loop_sides<T: Copy, const N: usize>(lp: &[T], c: [usize; N]) -> [Vec<T>; N] {
|
||||
std::array::from_fn(|k| {
|
||||
let (a, b) = (c[k], c.get(k + 1).copied().unwrap_or(c[0] + lp.len()));
|
||||
(a..b).filter_map(|i| lp.get(i % lp.len()).copied()).collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// The opposite sides of a mapped loop that are one curve each, as uses traversed the same way
|
||||
/// (the loop runs along one side and back along the other), for carrying a biased seed's
|
||||
/// spacing across ([`crate::seeds::carry`]).
|
||||
pub(crate) fn opposite_ties(lp: &[(usize, bool)], scheme: Scheme) -> Vec<[crate::seeds::Use; 2]> {
|
||||
let Scheme::Mapped(c) = scheme else { return Vec::new() };
|
||||
let [s0, s1, s2, s3] = loop_sides(lp, c);
|
||||
[(s0, s2), (s1, s3)]
|
||||
.into_iter()
|
||||
.filter_map(|(a, b)| match (a.as_slice(), b.as_slice()) {
|
||||
([a], [(b, fwd)]) => Some([*a, (*b, !fwd)]),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Add the scheme's constraints on the loops' curve variables.
|
||||
pub(crate) fn constrain(ia: &mut Intervals, loops: &[Vec<usize>], scheme: Scheme) {
|
||||
match scheme {
|
||||
|
||||
@@ -251,13 +251,34 @@ fn build(ctx: &Ctx, topo: &Topology, refine: u32) -> Result<Raw> {
|
||||
let loop_vars: Vec<Vec<usize>> = source.loops.iter().map(|lp| vars(lp)).collect();
|
||||
region::constrain(&mut ia, &loop_vars, scheme);
|
||||
let counts = ia.solve()?;
|
||||
// A biased seed along the sweep carries its layers to every side edge of its stage: a
|
||||
// linking face's right side runs up as its left side runs down, and the first side of each
|
||||
// stage runs up on every ring.
|
||||
let mut ties: Vec<[(usize, bool); 2]> = Vec::new();
|
||||
for stages in &topo.rings {
|
||||
for ring in stages {
|
||||
for l in ring {
|
||||
ties.push([l.right, (l.left.0, !l.left.1)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(first) = topo.rings.first() {
|
||||
for other in topo.rings.iter().skip(1).filter(|o| o.len() == first.len()) {
|
||||
for (a, b) in first.iter().zip(other) {
|
||||
if let (Some(a), Some(b)) = (a.first(), b.first()) {
|
||||
ties.push([a.right, b.right]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let carried = ctx.carried(&ties);
|
||||
// Edge nodes: B-rep vertices once, interiors on both faces of the edge.
|
||||
let mut nodes = Nodes { xyz: Vec::new() };
|
||||
let mut vertex_node: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
let mut edge_nodes: Vec<Vec<u32>> = Vec::with_capacity(brep.edges.len());
|
||||
for (e, edge) in brep.edges.iter().enumerate() {
|
||||
let n = counts.get(e).copied().unwrap_or(1);
|
||||
let pts = ctx.divide_edge(e, true, n)?;
|
||||
let pts = ctx.divide_edge(e, true, n, &carried)?;
|
||||
let last = pts.len().saturating_sub(1);
|
||||
let mut ids = Vec::with_capacity(pts.len());
|
||||
for (k, p) in pts.iter().enumerate() {
|
||||
|
||||
@@ -618,3 +618,43 @@ fn a_seed_on_a_shared_edge_holds_in_every_cell() {
|
||||
// Small at both ends, symmetric.
|
||||
assert!((z[1] - z[0] - (z[6] - z[5])).abs() < 1e-9 && z[3] - z[2] > z[1] - z[0], "{z:?}");
|
||||
}
|
||||
|
||||
/// A biased seed on one edge carries its spacing to the edges interval assignment ties to it
|
||||
/// (#257): the parallel edges of a mapped block, the opposite curves of an extruded
|
||||
/// cross-section's mapped region, the side edges of a sweep's linking faces, and the parallel
|
||||
/// edges of every cell they reach. The grid stays a grid: every node lies on one of the seed's
|
||||
/// planes.
|
||||
#[test]
|
||||
fn a_biased_seed_grades_the_edges_tied_to_it() {
|
||||
use fieldcraft_model::{Bias, BiasToward, SeedConstraint};
|
||||
let s = prism(&[[0.0, 0.0], [4.0, 0.0], [4.0, 2.0], [0.0, 2.0]], 0.0, 3.0);
|
||||
let bias = Some(Bias { ratio: 3.0, toward: BiasToward::Start });
|
||||
let all = |_: [f64; 3]| true;
|
||||
// Along x on the front bottom edge, small at x = 4 (the seed runs from 4 to 0).
|
||||
let x_seed = seed("x", vec![[4.0, 0.0, 0.0], [0.0, 0.0, 0.0]], 6, bias, SeedConstraint::Exact);
|
||||
for method in [HexMethod::Mapped, HexMethod::Extrude] {
|
||||
let m = hex_mesh(&s, &[], &UniformSize(1.0), &HexOptions { method, seeds: vec![x_seed.clone()], ..HexOptions::default() }).unwrap();
|
||||
let x = along(&m, 0, all);
|
||||
assert_eq!(x.len(), 7, "{method:?} {x:?}");
|
||||
assert!(x[6] - x[5] < x[1] - x[0], "{method:?} {x:?}");
|
||||
}
|
||||
// Through the thickness at x = y = 0, small at z = 0: the block mapped, and an L swept
|
||||
// (up z, the one way it sweeps).
|
||||
let l = prism(&[[0.0, 0.0], [6.0, 0.0], [6.0, 2.0], [2.0, 2.0], [2.0, 6.0], [0.0, 6.0]], 0.0, 3.0);
|
||||
let z_seed = seed("z", vec![[0.0, 0.0, 0.0], [0.0, 0.0, 3.0]], 6, bias, SeedConstraint::Exact);
|
||||
for (body, method) in [(&s, HexMethod::Mapped), (&l, HexMethod::Sweep)] {
|
||||
let m = hex_mesh(body, &[], &UniformSize(1.0), &HexOptions { method, seeds: vec![z_seed.clone()], ..HexOptions::default() }).unwrap();
|
||||
let z = along(&m, 2, all);
|
||||
assert_eq!(z.len(), 7, "{method:?} {z:?}");
|
||||
assert!(z[1] - z[0] < z[6] - z[5], "{method:?} {z:?}");
|
||||
}
|
||||
// Two cells: a seed on the far side of the first carries through their interface to the
|
||||
// far side of the second.
|
||||
let pairs = boxes(&[([0.0, 2.0], [0.0, 1.0]), ([2.0, 3.0], [0.0, 1.0])], 1.0);
|
||||
let none = vec![Vec::new(); 2];
|
||||
let opts = HexOptions { seeds: vec![seed("z", vec![[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], 6, bias, SeedConstraint::Exact)], ..HexOptions::default() };
|
||||
let m = hex_mesh_cells(&cells_of(&pairs, &none), &UniformSize(0.5), &opts).unwrap();
|
||||
let z = along(&m.mesh, 2, all);
|
||||
assert_eq!(z.len(), 7, "{z:?}");
|
||||
assert!(z[1] - z[0] < z[6] - z[5], "{z:?}");
|
||||
}
|
||||
@@ -154,6 +154,59 @@ pub(crate) fn place(poly: &[P], fractions: &[f64]) -> Result<Vec<P>> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A use of a curve: its index and whether it is traversed from its start.
|
||||
pub(crate) type Use = (usize, bool);
|
||||
|
||||
/// The spacing of biased seeds carried to the curves interval assignment ties to them (#257).
|
||||
///
|
||||
/// `own[c]` is curve `c`'s seed (any identity `T`) and whether the curve runs against it; each
|
||||
/// tie says two curve uses run side by side with the same count (opposite sides of a mapped face
|
||||
/// or a sweep's linking face, both traversed the same way along it). A biased seed's spacing
|
||||
/// spreads along the ties to every curve without a seed of its own, which it reaches through
|
||||
/// unseeded curves only; the curve takes the seed, oriented as the tie gives, so its nodes are
|
||||
/// placed as the seeded curve's are. Seeds are spread in curve order and the first one to reach a
|
||||
/// curve keeps it. Returns the carried seed of each unseeded curve (`None` elsewhere).
|
||||
pub(crate) fn carry<T: Clone>(own: &[Option<(T, bool)>], biased: impl Fn(&T) -> bool, ties: &[[Use; 2]]) -> Vec<Option<(T, bool)>> {
|
||||
let n = own.len();
|
||||
let mut next: Vec<Vec<(usize, bool, bool)>> = vec![Vec::new(); n];
|
||||
for &[(a, fa), (b, fb)] in ties {
|
||||
if a == b || a >= n || b >= n {
|
||||
continue;
|
||||
}
|
||||
// Traversed as tied, `a` runs forwards iff `fa`; `b`'s orientation follows from `a`'s.
|
||||
if let Some(l) = next.get_mut(a) {
|
||||
l.push((b, fa, fb));
|
||||
}
|
||||
if let Some(l) = next.get_mut(b) {
|
||||
l.push((a, fb, fa));
|
||||
}
|
||||
}
|
||||
let mut out: Vec<Option<(T, bool)>> = vec![None; n];
|
||||
for (start, seed) in own.iter().enumerate() {
|
||||
let Some((seed, rev)) = seed else { continue };
|
||||
if !biased(seed) {
|
||||
continue;
|
||||
}
|
||||
let mut stack = vec![(start, *rev)];
|
||||
while let Some((c, rev_c)) = stack.pop() {
|
||||
for &(d, fc, fd) in next.get(c).map(Vec::as_slice).unwrap_or(&[]) {
|
||||
if own.get(d).is_some_and(Option::is_some) || out.get(d).is_some_and(Option::is_some) {
|
||||
continue;
|
||||
}
|
||||
// Traversed as tied, `c` runs against the seed iff it runs forwards and against
|
||||
// it, or backwards and with it; `d` runs the same way as `c`.
|
||||
let against = rev_c == fc;
|
||||
let rev_d = if against { fd } else { !fd };
|
||||
if let Some(slot) = out.get_mut(d) {
|
||||
*slot = Some((seed.clone(), rev_d));
|
||||
}
|
||||
stack.push((d, rev_d));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The element size a seed asks for at each of its nodes (the mean length of the elements on
|
||||
/// either side), for size fields.
|
||||
pub fn node_sizes(seed: &EdgeSeed) -> Vec<(P, f64)> {
|
||||
@@ -211,4 +264,19 @@ mod tests {
|
||||
assert_eq!(h.len(), 4);
|
||||
assert!((h[0].1 - 10.0 * f[1]).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// A biased seed's spacing carries along the ties to unseeded curves, oriented as each tie
|
||||
/// says, and stops at a curve with a seed of its own (#257).
|
||||
#[test]
|
||||
fn biased_spacing_carries_along_ties() {
|
||||
// 0 runs against its seed; 0 forwards beside 1 backwards; 1 forwards beside 2 forwards;
|
||||
// 3 has its own seed and 4 lies beyond it.
|
||||
let own = vec![Some((0, true)), None, None, Some((1, false)), None, None];
|
||||
let ties = [[(0, true), (1, false)], [(1, true), (2, true)], [(2, true), (3, true)], [(3, true), (4, true)]];
|
||||
let biased = [true, true];
|
||||
let c = carry(&own, |&i: &usize| biased[i], &ties);
|
||||
assert_eq!(c, vec![None, Some((0, false)), Some((0, false)), None, Some((1, false)), None]);
|
||||
// Unbiased seeds keep to their own curve.
|
||||
assert!(carry(&own, |_: &usize| false, &ties).iter().all(Option::is_none));
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -161,7 +161,9 @@ edges of partition cells matching counts.
|
||||
both hold (opposite sides of a mapped face, the layers between two levels); the tet mesher
|
||||
divides the edge at the seed's nodes and grades its size field from them (its reply's
|
||||
`seeds` gives the mesh edges along each). A seed on an edge two partition cells share holds
|
||||
in both, so their meshes still conform.
|
||||
in both, so their meshes still conform. A biased seed's spacing carries to the unseeded edges
|
||||
tied to it (the parallel edges of a mapped block or cell, the opposite side of a mapped face,
|
||||
the other side edges of a sweep), so one biased edge grades the whole layer stack.
|
||||
4. `mesh.seed.list` lists the seeds and the edges they reach (`stale` when the design has no such
|
||||
edge); `mesh.seed.clear {edges?}` removes them. Seeds are saved with the model.
|
||||
|
||||
|
||||
Reference in new issue
Block a user