forked from 1b-agent/fieldcraft-upstream
mesh: the projection mesher takes fixed boundary patches as given (#490)
A patch of quads already meshed on a face of the body (a neighbour's mesh) is matched to the block faces on it: block vertices to its nodes (its singular nodes first, then along its grid lines), block edges along its grid lines, faces filled quad by quad. Those faces take the patch's divisions and nodes, held through smoothing. mesh.hex.project takes "fixed": [face names] and builds the patch from other bodies' meshes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
e887e3a8e5
commit
6f07e4c09c
7 files changed
+650
-6
No files matched your search
@@ -0,0 +1,422 @@
|
||||
//! Fixed boundary: an already-meshed patch of the body's surface taken as given.
|
||||
//!
|
||||
//! A neighbour meshed first (a corner cell mirrored onto its image, a cell next to an existing
|
||||
//! mesh) leaves quads on the face the two bodies share. The projection mesh of the second body
|
||||
//! keeps them: the block faces on that face take their divisions from the patch, their nodes are
|
||||
//! the patch's nodes (held through smoothing), and the two meshes meet node to node.
|
||||
//!
|
||||
//! The match is topological. Each block vertex on the patch goes to the nearest patch node; each
|
||||
//! block edge on it must run along one grid line of the patch from its first vertex's node to its
|
||||
//! second's (a straight walk: at every node the line goes on through the one neighbour that shares
|
||||
//! no quad with the edge it came in on); each block face is then filled quad by quad from two of
|
||||
//! its edges and must close on the other two. Every patch quad belongs to exactly one block face.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use super::lattice::face_key;
|
||||
use super::topology::{CORNERS, Topology, corner_at};
|
||||
use crate::vec3::{P, dist};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Most patches one projection takes.
|
||||
pub const MAX_PATCHES: usize = 64;
|
||||
|
||||
/// An already-meshed patch of the body's boundary, taken as given.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct FixedPatch {
|
||||
/// The B-rep faces of the body the patch covers.
|
||||
pub faces: Vec<u32>,
|
||||
pub nodes: Vec<P>,
|
||||
/// Its quads, by node index (either orientation).
|
||||
pub quads: Vec<[u32; 4]>,
|
||||
}
|
||||
|
||||
/// The block faces on the patches, and the patch nodes of their grids.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct Fixed {
|
||||
/// Per topology vertex on a patch, its node's position.
|
||||
pub vertex: BTreeMap<u32, P>,
|
||||
/// Per block edge on a patch (ascending vertex pair), its divisions.
|
||||
pub edge_divisions: BTreeMap<[u32; 2], usize>,
|
||||
/// Per block face on a patch, `(block, d, s)` and the positions of its grid: index
|
||||
/// `i + (n1 + 1) * j`, `i` along the first other direction of `d`, `j` along the second.
|
||||
pub faces: Vec<((usize, usize, usize), Vec<P>)>,
|
||||
/// Patch nodes taken.
|
||||
pub nodes: usize,
|
||||
}
|
||||
|
||||
/// One patch's quad mesh: the quads round each node and each node's neighbours.
|
||||
struct Grid<'a> {
|
||||
patch: &'a FixedPatch,
|
||||
quads_at: Vec<Vec<u32>>,
|
||||
nbrs: Vec<BTreeSet<u32>>,
|
||||
}
|
||||
|
||||
impl<'a> Grid<'a> {
|
||||
fn new(patch: &'a FixedPatch) -> Result<Grid<'a>> {
|
||||
let n = patch.nodes.len();
|
||||
let mut quads_at = vec![Vec::new(); n];
|
||||
let mut nbrs = vec![BTreeSet::new(); n];
|
||||
for (k, q) in patch.quads.iter().enumerate() {
|
||||
if q.iter().any(|&v| v as usize >= n) || q.iter().collect::<BTreeSet<_>>().len() != 4 {
|
||||
return Err(Error::Invalid(format!("fixed patch quad {k} names a node it does not have, or one twice")));
|
||||
}
|
||||
for i in 0..4 {
|
||||
let (a, b) = (q[i], q[(i + 1) % 4]);
|
||||
quads_at[a as usize].push(k as u32);
|
||||
nbrs[a as usize].insert(b);
|
||||
nbrs[b as usize].insert(a);
|
||||
}
|
||||
}
|
||||
Ok(Grid { patch, quads_at, nbrs })
|
||||
}
|
||||
|
||||
fn quad(&self, k: u32) -> [u32; 4] {
|
||||
self.patch.quads.get(k as usize).copied().unwrap_or([0; 4])
|
||||
}
|
||||
|
||||
/// The node after `p` on the grid line coming from `q`: the one neighbour of `p` in no quad
|
||||
/// with the edge `q`–`p`.
|
||||
fn straight_on(&self, q: u32, p: u32) -> Option<u32> {
|
||||
let beside: BTreeSet<u32> = self
|
||||
.quads_at
|
||||
.get(p as usize)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|&k| self.quad(k))
|
||||
.filter(|quad| quad.contains(&q))
|
||||
.flatten()
|
||||
.collect();
|
||||
let mut on = self.nbrs.get(p as usize)?.iter().filter(|r| **r != q && !beside.contains(r));
|
||||
let r = on.next()?;
|
||||
on.next().is_none().then_some(*r)
|
||||
}
|
||||
|
||||
/// The grid line from `a` to `b`: one of `a`'s neighbours, walked straight on, reaches `b`.
|
||||
fn line(&self, a: u32, b: u32) -> Option<Vec<u32>> {
|
||||
let cap = self.patch.nodes.len();
|
||||
let mut found: Option<Vec<u32>> = None;
|
||||
for &first in self.nbrs.get(a as usize)? {
|
||||
let mut line = vec![a, first];
|
||||
while line.len() <= cap {
|
||||
let (Some(&p), Some(&q)) = (line.last(), line.get(line.len().wrapping_sub(2))) else { break };
|
||||
if p == b {
|
||||
break;
|
||||
}
|
||||
match self.straight_on(q, p) {
|
||||
Some(r) => line.push(r),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
if line.last() == Some(&b) && found.as_ref().is_none_or(|f| line.len() < f.len()) {
|
||||
found = Some(line);
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// The nodes no block face can hold inside: on the patch's boundary, those in other than two
|
||||
/// quads (its corners); inside it, those in other than four.
|
||||
fn singular(&self) -> Vec<u32> {
|
||||
let mut edge_quads: HashMap<[u32; 2], usize> = HashMap::new();
|
||||
for q in &self.patch.quads {
|
||||
for i in 0..4 {
|
||||
let (a, b) = (q[i], q[(i + 1) % 4]);
|
||||
*edge_quads.entry([a.min(b), a.max(b)]).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
let mut on_boundary = vec![false; self.patch.nodes.len()];
|
||||
for (e, n) in &edge_quads {
|
||||
if *n == 1 {
|
||||
for v in e {
|
||||
if let Some(slot) = on_boundary.get_mut(*v as usize) {
|
||||
*slot = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(0..self.patch.nodes.len() as u32)
|
||||
.filter(|&v| {
|
||||
let quads = self.quads_at.get(v as usize).map_or(0, Vec::len);
|
||||
quads > 0 && quads != if on_boundary.get(v as usize).copied().unwrap_or(false) { 2 } else { 4 }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The nodes of the grid lines from `a`, each walked straight on to its end (at most the
|
||||
/// patch's node count per line).
|
||||
fn lines_from(&self, a: u32) -> Vec<Vec<u32>> {
|
||||
let cap = self.patch.nodes.len();
|
||||
let mut out = Vec::new();
|
||||
for &first in self.nbrs.get(a as usize).into_iter().flatten() {
|
||||
let mut line = vec![a, first];
|
||||
while line.len() <= cap {
|
||||
let (Some(&p), Some(&q)) = (line.last(), line.get(line.len().wrapping_sub(2))) else { break };
|
||||
match self.straight_on(q, p) {
|
||||
Some(r) if r != a => line.push(r),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The patch node of each block vertex in `vertices` (positions `vpos`, joined by the block
|
||||
/// edges `edges`). A node no block face can hold inside is a block vertex: each goes to the
|
||||
/// nearest vertex. Then, out from the vertices placed, a vertex joined to a placed one goes
|
||||
/// to the node nearest it on the grid lines from the placed one's node. Any left go to their
|
||||
/// nearest node.
|
||||
fn match_vertices(&self, vertices: &[u32], edges: &BTreeSet<[u32; 2]>, vpos: &[P]) -> BTreeMap<u32, u32> {
|
||||
let at = |v: u32| vpos.get(v as usize).copied().unwrap_or_default();
|
||||
let node = |n: u32| self.patch.nodes.get(n as usize).copied().unwrap_or_default();
|
||||
let mut node_of: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
// Singular nodes to vertices, nearest pairs first.
|
||||
let mut pairs: Vec<(f64, u32, u32)> =
|
||||
self.singular().into_iter().flat_map(|n| vertices.iter().map(move |&v| (n, v))).map(|(n, v)| (dist(node(n), at(v)), v, n)).collect();
|
||||
pairs.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let mut taken: BTreeSet<u32> = BTreeSet::new();
|
||||
for (_, v, n) in pairs {
|
||||
if !node_of.contains_key(&v) && !taken.contains(&n) {
|
||||
node_of.insert(v, n);
|
||||
taken.insert(n);
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let mut grew = false;
|
||||
for &[a, b] in edges {
|
||||
let (from, to) = match (node_of.get(&a), node_of.get(&b)) {
|
||||
(Some(&na), None) => (na, b),
|
||||
(None, Some(&nb)) => (nb, a),
|
||||
_ => continue,
|
||||
};
|
||||
let best = self
|
||||
.lines_from(from)
|
||||
.into_iter()
|
||||
.flat_map(|l| l.into_iter().skip(1))
|
||||
.filter(|n| !taken.contains(n))
|
||||
.min_by(|x, y| dist(node(*x), at(to)).total_cmp(&dist(node(*y), at(to))));
|
||||
if let Some(n) = best {
|
||||
node_of.insert(to, n);
|
||||
taken.insert(n);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
if !grew {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for &v in vertices {
|
||||
if !node_of.contains_key(&v) {
|
||||
let nearest = (0..self.patch.nodes.len() as u32).filter(|n| !taken.contains(n)).min_by(|x, y| dist(node(*x), at(v)).total_cmp(&dist(node(*y), at(v))));
|
||||
if let Some(n) = nearest {
|
||||
node_of.insert(v, n);
|
||||
taken.insert(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
node_of
|
||||
}
|
||||
|
||||
/// The fourth corner of the quad holding `a`, `b` and `c` (`a` the corner between them), and
|
||||
/// the quad.
|
||||
fn complete(&self, a: u32, b: u32, c: u32) -> Option<(u32, u32)> {
|
||||
let k = *self.quads_at.get(a as usize)?.iter().find(|&&k| {
|
||||
let q = self.quad(k);
|
||||
q.contains(&b) && q.contains(&c)
|
||||
})?;
|
||||
let d = self.quad(k).into_iter().find(|v| ![a, b, c].contains(v))?;
|
||||
Some((d, k))
|
||||
}
|
||||
}
|
||||
|
||||
/// The block-local corner of face `(d, s)` at `(i, j)` (0 or 1) along the face's two directions.
|
||||
fn face_corner(block: &[u32; 8], d: usize, s: usize, i: usize, j: usize) -> u32 {
|
||||
let (e1, e2) = super::other(d);
|
||||
let mut bits = [0; 3];
|
||||
bits[d] = s;
|
||||
bits[e1] = i;
|
||||
bits[e2] = j;
|
||||
block[corner_at(bits)]
|
||||
}
|
||||
|
||||
/// Match the block faces whose targets lie on the patches: `outer` lists the outer block faces
|
||||
/// `(block, d, s)`, `target_of` gives a face's target set of B-rep faces, `vpos` the projected
|
||||
/// vertex positions.
|
||||
pub(crate) fn match_patches(
|
||||
t: &Topology,
|
||||
outer: &[(usize, usize, usize)],
|
||||
target_of: &dyn Fn(&[u32; 4]) -> Option<Vec<u32>>,
|
||||
patches: &[FixedPatch],
|
||||
vpos: &[P],
|
||||
) -> Result<Fixed> {
|
||||
if patches.len() > MAX_PATCHES {
|
||||
return Err(Error::Invalid(format!("at most {MAX_PATCHES} fixed patches")));
|
||||
}
|
||||
let mut fixed = Fixed::default();
|
||||
for (pi, patch) in patches.iter().enumerate() {
|
||||
let grid = Grid::new(patch)?;
|
||||
let on: BTreeSet<u32> = patch.faces.iter().copied().collect();
|
||||
// The block faces on this patch.
|
||||
let mut faces: Vec<(usize, usize, usize)> = Vec::new();
|
||||
for &(b, d, s) in outer {
|
||||
let Some(block) = t.blocks.get(b) else { continue };
|
||||
let Some(set) = target_of(&face_key(block, d, s)) else { continue };
|
||||
let inside = set.iter().filter(|f| on.contains(f)).count();
|
||||
if inside == 0 {
|
||||
continue;
|
||||
}
|
||||
if inside < set.len() {
|
||||
return Err(Error::Invalid(format!(
|
||||
"fixed patch {pi}: face {} of block {b} projects onto B-rep faces {set:?}, only some of them fixed; fix all of its smooth group or none",
|
||||
super::topology::face_number(d, s)
|
||||
)));
|
||||
}
|
||||
faces.push((b, d, s));
|
||||
}
|
||||
if faces.is_empty() {
|
||||
return Err(Error::Invalid(format!("fixed patch {pi}: no block face projects onto its faces {:?}", patch.faces)));
|
||||
}
|
||||
// Block vertices to patch nodes.
|
||||
let mut on_face: Vec<u32> = Vec::new();
|
||||
let mut block_edges: BTreeSet<[u32; 2]> = BTreeSet::new();
|
||||
for &(b, d, s) in &faces {
|
||||
let block = t.blocks.get(b).copied().unwrap_or([0; 8]);
|
||||
let cyc = [(0, 0), (1, 0), (1, 1), (0, 1)].map(|(i, j)| face_corner(&block, d, s, i, j));
|
||||
for k in 0..4 {
|
||||
on_face.push(cyc[k]);
|
||||
block_edges.insert([cyc[k].min(cyc[(k + 1) % 4]), cyc[k].max(cyc[(k + 1) % 4])]);
|
||||
}
|
||||
}
|
||||
on_face.sort_unstable();
|
||||
on_face.dedup();
|
||||
let node_of = grid.match_vertices(&on_face, &block_edges, vpos);
|
||||
let taken: BTreeSet<u32> = node_of.values().copied().collect();
|
||||
if taken.len() != node_of.len() {
|
||||
return Err(Error::Failed(format!(
|
||||
"fixed patch {pi}: two block vertices fall on one patch node; the blocks there do not match the patch's mesh"
|
||||
)));
|
||||
}
|
||||
// Block edges along grid lines.
|
||||
let mut lines: BTreeMap<[u32; 2], Vec<u32>> = BTreeMap::new();
|
||||
for &(b, d, s) in &faces {
|
||||
let block = t.blocks.get(b).copied().unwrap_or([0; 8]);
|
||||
let cyc = [(0, 0), (1, 0), (1, 1), (0, 1)].map(|(i, j)| face_corner(&block, d, s, i, j));
|
||||
for k in 0..4 {
|
||||
let (a, c) = (cyc[k].min(cyc[(k + 1) % 4]), cyc[k].max(cyc[(k + 1) % 4]));
|
||||
if lines.contains_key(&[a, c]) {
|
||||
continue;
|
||||
}
|
||||
let (Some(&na), Some(&nc)) = (node_of.get(&a), node_of.get(&c)) else { continue };
|
||||
let line = grid.line(na, nc).ok_or_else(|| {
|
||||
Error::Failed(format!(
|
||||
"fixed patch {pi}: the block edge from vertex {a} to {c} (face {} of block {b}) does not follow a grid line of the patch; the blocks there do not match the patch's mesh",
|
||||
super::topology::face_number(d, s)
|
||||
))
|
||||
})?;
|
||||
lines.insert([a, c], line);
|
||||
}
|
||||
}
|
||||
// Each face filled quad by quad from its i and j edges at the 0 corner.
|
||||
let mut used: BTreeSet<u32> = BTreeSet::new();
|
||||
for &(b, d, s) in &faces {
|
||||
let block = t.blocks.get(b).copied().unwrap_or([0; 8]);
|
||||
let corner = |i, j| face_corner(&block, d, s, i, j);
|
||||
// The edge from corner (i0, j0) to (i1, j1), in that order.
|
||||
let edge = |from: u32, to: u32| -> Vec<u32> {
|
||||
let line = lines.get(&[from.min(to), from.max(to)]).cloned().unwrap_or_default();
|
||||
if from < to { line } else { line.into_iter().rev().collect() }
|
||||
};
|
||||
let (bottom, left) = (edge(corner(0, 0), corner(1, 0)), edge(corner(0, 0), corner(0, 1)));
|
||||
let (top, right) = (edge(corner(0, 1), corner(1, 1)), edge(corner(1, 0), corner(1, 1)));
|
||||
let (n1, n2) = (bottom.len().saturating_sub(1), left.len().saturating_sub(1));
|
||||
let mismatch = || {
|
||||
Error::Failed(format!(
|
||||
"fixed patch {pi}: face {} of block {b} does not close on the patch's mesh ({n1} × {n2} divisions from two of its edges); the blocks there do not match the patch's mesh",
|
||||
super::topology::face_number(d, s)
|
||||
))
|
||||
};
|
||||
if n1 == 0 || n2 == 0 || top.len() != n1 + 1 || right.len() != n2 + 1 {
|
||||
return Err(mismatch());
|
||||
}
|
||||
let mut g: Vec<u32> = vec![u32::MAX; (n1 + 1) * (n2 + 1)];
|
||||
let at = |i: usize, j: usize| i + (n1 + 1) * j;
|
||||
for (i, &v) in bottom.iter().enumerate() {
|
||||
g[at(i, 0)] = v;
|
||||
}
|
||||
for (j, &v) in left.iter().enumerate() {
|
||||
g[at(0, j)] = v;
|
||||
}
|
||||
for j in 0..n2 {
|
||||
for i in 0..n1 {
|
||||
let (a, x, y) = (g[at(i, j)], g[at(i + 1, j)], g[at(i, j + 1)]);
|
||||
let (dn, k) = grid.complete(a, x, y).ok_or_else(mismatch)?;
|
||||
if !used.insert(k) {
|
||||
return Err(mismatch());
|
||||
}
|
||||
g[at(i + 1, j + 1)] = dn;
|
||||
}
|
||||
}
|
||||
let closes = (0..=n1).all(|i| g[at(i, n2)] == top[i]) && (0..=n2).all(|j| g[at(n1, j)] == right[j]);
|
||||
if !closes {
|
||||
return Err(mismatch());
|
||||
}
|
||||
for (pair, line) in [([corner(0, 0), corner(1, 0)], &bottom), ([corner(0, 0), corner(0, 1)], &left)] {
|
||||
fixed.edge_divisions.insert([pair[0].min(pair[1]), pair[0].max(pair[1])], line.len() - 1);
|
||||
}
|
||||
for (pair, line) in [([corner(0, 1), corner(1, 1)], &top), ([corner(1, 0), corner(1, 1)], &right)] {
|
||||
fixed.edge_divisions.insert([pair[0].min(pair[1]), pair[0].max(pair[1])], line.len() - 1);
|
||||
}
|
||||
let positions = g.iter().map(|&v| patch.nodes.get(v as usize).copied().unwrap_or_default()).collect();
|
||||
fixed.faces.push(((b, d, s), positions));
|
||||
}
|
||||
if used.len() != patch.quads.len() {
|
||||
return Err(Error::Failed(format!(
|
||||
"fixed patch {pi}: the block faces on it cover {} of its {} quads; the patch reaches beyond the faces it names, or the blocks do not match its mesh",
|
||||
used.len(),
|
||||
patch.quads.len()
|
||||
)));
|
||||
}
|
||||
let mut nodes: BTreeSet<u32> = BTreeSet::new();
|
||||
for k in &used {
|
||||
nodes.extend(grid.quad(*k));
|
||||
}
|
||||
fixed.nodes += nodes.len();
|
||||
for (v, n) in node_of {
|
||||
if let Some(p) = patch.nodes.get(n as usize) {
|
||||
fixed.vertex.insert(v, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(fixed)
|
||||
}
|
||||
|
||||
/// The class divisions the patches set: per class, the count every fixed edge of it has. Edges of
|
||||
/// one class with different counts on the patches are an error.
|
||||
pub(crate) fn class_divisions(t: &Topology, classes: &[[usize; 3]], fixed: &Fixed) -> Result<HashMap<usize, usize>> {
|
||||
let mut out: HashMap<usize, usize> = HashMap::new();
|
||||
for (block, cls) in t.blocks.iter().zip(classes) {
|
||||
for (d, &class) in cls.iter().enumerate() {
|
||||
for (k, bits) in CORNERS.iter().enumerate() {
|
||||
if bits[d] != 0 {
|
||||
continue;
|
||||
}
|
||||
let mut e = *bits;
|
||||
e[d] = 1;
|
||||
let (a, b) = (block[k], block[corner_at(e)]);
|
||||
let Some(&n) = fixed.edge_divisions.get(&[a.min(b), a.max(b)]) else { continue };
|
||||
match out.get(&class) {
|
||||
Some(&m) if m != n => {
|
||||
return Err(Error::Failed(format!(
|
||||
"the fixed patches give parallel block edges {m} and {n} divisions (the edge from vertex {a} to {b}); a structured block mesh cannot meet both"
|
||||
)));
|
||||
}
|
||||
_ => {
|
||||
out.insert(class, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
//! index space, carved, projected face → surface, edge → curve, vertex → point, then smoothed);
|
||||
//! the algorithms are the papers cited in each module.
|
||||
|
||||
pub mod fixed;
|
||||
mod lattice;
|
||||
pub mod smooth;
|
||||
pub mod target;
|
||||
@@ -29,6 +30,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use fieldcraft_model::ElementKind;
|
||||
|
||||
pub use fixed::FixedPatch;
|
||||
pub use smooth::{QUALITY_FLOOR, SmoothReport, Smoothing};
|
||||
pub use target::Geometry;
|
||||
pub use topology::Topology;
|
||||
@@ -89,6 +91,9 @@ pub struct ProjectOptions {
|
||||
/// The smallest scaled Jacobian accepted: a mesh with a worse element is an error that
|
||||
/// says where a template is missing (0 accepts any valid mesh).
|
||||
pub min_quality: f64,
|
||||
/// Already-meshed patches of the boundary taken as given ([`fixed`]): the block faces on
|
||||
/// them take their divisions and nodes from the patch, held through smoothing.
|
||||
pub fixed: Vec<FixedPatch>,
|
||||
}
|
||||
|
||||
impl Default for ProjectOptions {
|
||||
@@ -102,6 +107,7 @@ impl Default for ProjectOptions {
|
||||
iterations: 100,
|
||||
max_elements: 2_000_000,
|
||||
min_quality: DEFAULT_MIN_QUALITY,
|
||||
fixed: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,6 +131,8 @@ pub struct ProjectReport {
|
||||
pub targets: Vec<Vec<u32>>,
|
||||
/// Divisions per block.
|
||||
pub divisions: Vec<[usize; 3]>,
|
||||
/// Nodes taken from the fixed patches, held where they are.
|
||||
pub fixed_nodes: usize,
|
||||
}
|
||||
|
||||
/// An all-hex mesh of a projected topology.
|
||||
@@ -383,6 +391,18 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
for (v, p) in vpos.iter_mut().enumerate() {
|
||||
*p = place(g, &targets, vertex_c(v as u32), *p, tol);
|
||||
}
|
||||
// Vertices on fixed patches go to their patch nodes.
|
||||
let fixed = if opts.fixed.is_empty() {
|
||||
fixed::Fixed::default()
|
||||
} else {
|
||||
let target_of = |k: &[u32; 4]| face_target.get(k).and_then(|&i| targets.get(i)).cloned();
|
||||
fixed::match_patches(t, &outer, &target_of, &opts.fixed, &vpos)?
|
||||
};
|
||||
for (&v, &p) in &fixed.vertex {
|
||||
if let Some(slot) = vpos.get_mut(v as usize) {
|
||||
*slot = p;
|
||||
}
|
||||
}
|
||||
// Inner vertices follow: the projected vertices' moves blended through the block edges
|
||||
// (a bounded Jacobi solve of Laplace's equation for the displacement), so a block the
|
||||
// projection stretches does not leave its inner corners behind.
|
||||
@@ -535,6 +555,14 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
*slot = n;
|
||||
}
|
||||
}
|
||||
for (class, n) in fixed::class_divisions(t, &classes, &fixed)? {
|
||||
if let Some(slot) = class_n.get_mut(class) {
|
||||
if opts.divisions.iter().any(|&(b, d, _)| classes.get(b).and_then(|c| c.get(d)) == Some(&class)) && *slot != n {
|
||||
return Err(Error::Invalid(format!("divisions set by hand ({}) differ from the fixed patch's ({n})", *slot)));
|
||||
}
|
||||
*slot = n;
|
||||
}
|
||||
}
|
||||
let divisions: Vec<[usize; 3]> = classes.iter().map(|c| c.map(|k| class_n.get(k).copied().unwrap_or(1))).collect();
|
||||
let lat = build(t, &divisions, opts.max_elements)?;
|
||||
|
||||
@@ -552,8 +580,30 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
let n = lat.entity.len();
|
||||
let mut nodes = vec![[0.0; 3]; n];
|
||||
let mut placed = vec![false; n];
|
||||
// Nodes on fixed patches: placed and held.
|
||||
let mut held = vec![false; n];
|
||||
for ((b, d, s), grid) in &fixed.faces {
|
||||
let (Some(nd), Some(bn)) = (lat.divisions.get(*b), lat.block_nodes.get(*b)) else { continue };
|
||||
let (e1, e2) = other(*d);
|
||||
for j in 0..=nd[e2] {
|
||||
for i in 0..=nd[e1] {
|
||||
let mut at = [0; 3];
|
||||
at[*d] = s * nd[*d];
|
||||
at[e1] = i;
|
||||
at[e2] = j;
|
||||
let (Some(&id), Some(&p)) = (bn.get(crate::quad::lattice_index(*nd, at)), grid.get(i + (nd[e1] + 1) * j)) else {
|
||||
return Err(Error::Failed("a fixed patch's grid does not fit its block face".into()));
|
||||
};
|
||||
if let (Some(slot), Some(h), Some(pl)) = (nodes.get_mut(id as usize), held.get_mut(id as usize), placed.get_mut(id as usize)) {
|
||||
*slot = p;
|
||||
*h = true;
|
||||
*pl = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, e) in lat.entity.iter().enumerate() {
|
||||
if let Entity::Vertex(v) = e {
|
||||
if let (Entity::Vertex(v), false) = (e, held[i]) {
|
||||
nodes[i] = vpos.get(*v as usize).copied().unwrap_or_default();
|
||||
placed[i] = true;
|
||||
}
|
||||
@@ -655,7 +705,7 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
}
|
||||
let boundary_quads: Vec<[u32; 4]> = quads.iter().map(|q| q.0).collect();
|
||||
let smoothing = smooth::smooth(
|
||||
smooth::Mesh { nodes: &mut nodes, hexes: &lat.hexes, constraint: &constraint, boundary: &boundary_quads },
|
||||
smooth::Mesh { nodes: &mut nodes, hexes: &lat.hexes, constraint: &constraint, held: &held, boundary: &boundary_quads },
|
||||
g,
|
||||
&targets,
|
||||
opts.smoothing,
|
||||
@@ -709,6 +759,7 @@ pub fn project_mesh(t: &Topology, surface: &BoundarySurface, g: &dyn Geometry, o
|
||||
surface_volume,
|
||||
targets: targets.clone(),
|
||||
divisions: lat.divisions.clone(),
|
||||
fixed_nodes: held.iter().filter(|h| **h).count(),
|
||||
};
|
||||
// Where the worst element is, for the error messages.
|
||||
let worst = || {
|
||||
|
||||
@@ -287,6 +287,8 @@ pub(crate) struct Mesh<'a> {
|
||||
pub nodes: &'a mut Vec<P>,
|
||||
pub hexes: &'a [[u32; 8]],
|
||||
pub constraint: &'a [Constraint],
|
||||
/// Nodes held where they are (a fixed patch's).
|
||||
pub held: &'a [bool],
|
||||
/// Boundary quads (outward), for the neighbours of boundary nodes.
|
||||
pub boundary: &'a [[u32; 4]],
|
||||
}
|
||||
@@ -403,6 +405,7 @@ fn pass(m: &mut Mesh, g: &dyn Geometry, targets: &[Vec<u32>], method: Smoothing,
|
||||
}
|
||||
}
|
||||
let cons = |v: u32| m.constraint.get(v as usize).copied().unwrap_or(Constraint::Free);
|
||||
let is_held = |v: u32| m.held.get(v as usize).copied().unwrap_or(false);
|
||||
let mut stencils: Vec<Option<Stencil>> = vec![None; n];
|
||||
if matches!(method, Smoothing::Elliptic | Smoothing::EllipticControlled) {
|
||||
for v in 0..n as u32 {
|
||||
@@ -445,6 +448,9 @@ fn pass(m: &mut Mesh, g: &dyn Geometry, targets: &[Vec<u32>], method: Smoothing,
|
||||
report.iterations = it + 1;
|
||||
let mut moved = 0.0f64;
|
||||
for v in 0..n as u32 {
|
||||
if is_held(v) {
|
||||
continue;
|
||||
}
|
||||
let c = cons(v);
|
||||
let p = at(m.nodes, v);
|
||||
let mean = |nbrs: &mut dyn Iterator<Item = u32>| -> Option<P> {
|
||||
@@ -531,7 +537,7 @@ fn pass(m: &mut Mesh, g: &dyn Geometry, targets: &[Vec<u32>], method: Smoothing,
|
||||
let mut improved = false;
|
||||
for &v in &poor {
|
||||
let c = cons(v);
|
||||
if matches!(c, Constraint::Corner(..)) {
|
||||
if matches!(c, Constraint::Corner(..)) || is_held(v) {
|
||||
continue;
|
||||
}
|
||||
let p = at(m.nodes, v);
|
||||
@@ -577,7 +583,7 @@ fn pass(m: &mut Mesh, g: &dyn Geometry, targets: &[Vec<u32>], method: Smoothing,
|
||||
let mut moved = false;
|
||||
for v in 0..n as u32 {
|
||||
let c = cons(v);
|
||||
if !matches!(c, Constraint::Surface(_)) {
|
||||
if !matches!(c, Constraint::Surface(_)) || is_held(v) {
|
||||
continue;
|
||||
}
|
||||
let p = at(m.nodes, v);
|
||||
|
||||
@@ -273,3 +273,68 @@ fn hostile_topologies_are_errors() {
|
||||
let mut g3 = Topology::grid([&[0.0, 1.0], &[0.0, 1.0], &[0.0, 1.0]]).unwrap();
|
||||
assert!(g3.carve([1, 1, 1], &[[[0, 0], [0, 0], [0, 0]]]).is_err());
|
||||
}
|
||||
|
||||
/// The quads of `m` on B-rep face `face`, as a patch over its own nodes.
|
||||
fn patch_of(m: &ProjectedMesh, face: u32) -> FixedPatch {
|
||||
let mut index: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
|
||||
let mut patch = FixedPatch { faces: vec![face], ..FixedPatch::default() };
|
||||
for b in m.boundary.iter().filter(|b| b.brep_face == face) {
|
||||
let hex = m.hexes[b.element as usize];
|
||||
let local = ElementKind::Hex8.faces()[b.face as usize - 1];
|
||||
let q = [0, 1, 2, 3].map(|i| {
|
||||
let v = hex[local[i]];
|
||||
*index.entry(v).or_insert_with(|| {
|
||||
patch.nodes.push(m.nodes[v as usize]);
|
||||
patch.nodes.len() as u32 - 1
|
||||
})
|
||||
});
|
||||
patch.quads.push(q);
|
||||
}
|
||||
patch
|
||||
}
|
||||
|
||||
/// A fixed patch is taken as given (#490): a disc meshed against the top quads of another disc's
|
||||
/// mesh (moved down onto its bottom face, as the face two stacked bodies share) keeps those nodes
|
||||
/// exactly and their divisions, though its own size asks for coarser elements, and smooths the
|
||||
/// rest around them.
|
||||
#[test]
|
||||
fn a_fixed_patch_sets_the_nodes_and_divisions_of_the_faces_on_it() {
|
||||
let (r, h) = (10.0, 2.0);
|
||||
let (s, t) = cylinder(r, h, 128);
|
||||
let topo = Topology::butterfly([0.0; 3], [0.0, 0.0, 1.0], r, &[0.0, h], 0.55).unwrap();
|
||||
let below = project_mesh(&topo, &s, &t, &options(0.75)).unwrap();
|
||||
let mut patch = patch_of(&below, 1);
|
||||
for p in &mut patch.nodes {
|
||||
p[2] -= h;
|
||||
}
|
||||
patch.faces = vec![0];
|
||||
let m = project_mesh(&topo, &s, &t, &ProjectOptions { fixed: vec![patch.clone()], ..options(1.5) }).unwrap();
|
||||
assert_eq!(m.report.fixed_nodes, patch.nodes.len());
|
||||
assert!(m.report.min_scaled_jacobian >= 0.5, "{:?}", m.report);
|
||||
let bottom = patch_of(&m, 0);
|
||||
assert_eq!(bottom.quads.len(), patch.quads.len());
|
||||
for p in &patch.nodes {
|
||||
assert!(bottom.nodes.iter().any(|q| dist(*p, *q) == 0.0), "{p:?} is not a node of the mesh");
|
||||
}
|
||||
// The top is meshed at the patch's divisions, not the coarser size's.
|
||||
assert_eq!(patch_of(&m, 1).quads.len(), patch.quads.len());
|
||||
}
|
||||
|
||||
/// A patch the blocks do not match is an error that names the patch, not a mesh that leaves a
|
||||
/// gap: here half the bottom's quads, and a patch on a face no block lies on.
|
||||
#[test]
|
||||
fn a_fixed_patch_the_blocks_do_not_match_is_an_error() {
|
||||
let (r, h) = (10.0, 2.0);
|
||||
let (s, t) = cylinder(r, h, 128);
|
||||
let topo = Topology::butterfly([0.0; 3], [0.0, 0.0, 1.0], r, &[0.0, h], 0.55).unwrap();
|
||||
let m = project_mesh(&topo, &s, &t, &options(1.0)).unwrap();
|
||||
let mut half = patch_of(&m, 0);
|
||||
half.quads.retain(|q| q.iter().all(|&v| half.nodes[v as usize][0] <= 1e-9));
|
||||
let e = project_mesh(&topo, &s, &t, &ProjectOptions { fixed: vec![half], ..options(1.0) }).unwrap_err().to_string();
|
||||
assert!(e.contains("fixed patch 0"), "{e}");
|
||||
let elsewhere = FixedPatch { faces: vec![7], ..patch_of(&m, 0) };
|
||||
let e = project_mesh(&topo, &s, &t, &ProjectOptions { fixed: vec![elsewhere], ..options(1.0) }).unwrap_err().to_string();
|
||||
assert!(e.contains("fixed patch 0"), "{e}");
|
||||
let broken = FixedPatch { quads: vec![[0, 0, 1, 2]], ..patch_of(&m, 0) };
|
||||
assert!(project_mesh(&topo, &s, &t, &ProjectOptions { fixed: vec![broken], ..options(1.0) }).is_err());
|
||||
}
|
||||
Reference in new issue
Block a user