forked from 1b-agent/fieldcraft-upstream
hex: conforming multi-block meshes of partitioned parts
hex_mesh_cells meshes cells that meet face to face (SolidCraft split bodies) as one conforming all-hex mesh: each cell a mapped block after smoothly meeting faces are joined (virtual topology: the halves of a hole, a plane running into a fillet), vertices, edges and faces shared by identity, interval assignment over all cells, each shared face meshed once, radial edges graded off curved faces. finish spans several cells: tags per cell, hex20 mid-edge nodes shared across cells, a closed 2-manifold boundary and no free internal face. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
8ca2dddeec
commit
b6ce7aee69
8 files changed
+1439
-81
No files matched your search
+208
-36
@@ -19,6 +19,11 @@
|
||||
//! A body none of these fits is [`Error::Unsupported`] with each method's reason: the input for
|
||||
//! decomposition into sweepable and mappable regions.
|
||||
//!
|
||||
//! **Partitioned parts.** [`hex_mesh_cells`] ([`cells`]) meshes a part split into cells that
|
||||
//! meet face to face (SolidCraft split bodies) as one conforming mesh: every cell a mapped block,
|
||||
//! interval assignment over all cells together, each shared face meshed once and its nodes
|
||||
//! merged by identity.
|
||||
//!
|
||||
//! **Common steps.** Interval assignment ([`interval`]) picks the division counts of shared curves
|
||||
//! together. Boundary nodes on analytic faces are placed on the exact surface; quadratic hexahedra
|
||||
//! (hex20) get their mid-edge nodes projected onto the faces (and onto both faces of a B-rep
|
||||
@@ -31,6 +36,7 @@
|
||||
|
||||
mod arrangement;
|
||||
mod brep;
|
||||
mod cells;
|
||||
mod geom;
|
||||
mod interval;
|
||||
mod layered;
|
||||
@@ -43,6 +49,7 @@ mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use cells::{Cell, CellsMesh, GROWTH, MAX_CELLS, hex_mesh_cells};
|
||||
pub use geom::Surface;
|
||||
|
||||
use brep::Brep;
|
||||
@@ -198,16 +205,7 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
|
||||
surface.shells().len()
|
||||
)));
|
||||
}
|
||||
// A mesh far beyond the element limit is refused before any work: the solid's volume over
|
||||
// the cube of the smallest size the field asks for at the boundary.
|
||||
let mut h_min = f64::INFINITY;
|
||||
for p in surface.points() {
|
||||
h_min = h_min.min(crate::size::checked_size(field, *p)?);
|
||||
}
|
||||
let estimate = surface.volume().abs() / h_min.powi(3);
|
||||
if !(estimate <= 8.0 * opts.max_elements as f64) {
|
||||
return Err(Error::Limit(format!("about {estimate:.3e} hexahedra at this size; the limit is {}", opts.max_elements)));
|
||||
}
|
||||
check_estimate(&[surface], field, opts)?;
|
||||
let brep = Brep::new(surface, faces)?;
|
||||
let tree = TriangleTree::new(surface)?;
|
||||
let ctx = Ctx { brep: &brep, tree: &tree, field, opts, tol: RELATIVE_TOLERANCE * brep.diagonal };
|
||||
@@ -253,7 +251,7 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
|
||||
Err(e @ Error::Limit(_)) => return Err(e),
|
||||
Err(e) => last = Some(e),
|
||||
},
|
||||
_ => match mapped::mesh(&ctx).and_then(|raw| finish(&ctx, raw, method)) {
|
||||
_ => match mapped::mesh(&ctx).and_then(|raw| finish(&Cells::one(&ctx), raw, method)) {
|
||||
Ok(mesh) => return Ok(mesh),
|
||||
Err(e @ Error::Limit(_)) => return Err(e),
|
||||
Err(e) => last = Some(e),
|
||||
@@ -266,6 +264,23 @@ pub fn hex_mesh(surface: &BoundarySurface, faces: &[Option<Surface>], field: &dy
|
||||
Err(Error::Unsupported(format!("no structured hex method fits this body ({})", reasons.join("; "))))
|
||||
}
|
||||
|
||||
/// Refuse a mesh far beyond the element limit before any work: the solids' volume over the cube
|
||||
/// of the smallest size the field asks for on their boundaries.
|
||||
fn check_estimate(surfaces: &[&BoundarySurface], field: &dyn SizeField, opts: &HexOptions) -> Result<()> {
|
||||
let (mut h_min, mut volume) = (f64::INFINITY, 0.0);
|
||||
for s in surfaces {
|
||||
for p in s.points() {
|
||||
h_min = h_min.min(crate::size::checked_size(field, *p)?);
|
||||
}
|
||||
volume += s.volume().abs();
|
||||
}
|
||||
let estimate = volume / h_min.powi(3);
|
||||
if !(estimate <= 8.0 * opts.max_elements as f64) {
|
||||
return Err(Error::Limit(format!("about {estimate:.3e} hexahedra at this size; the limit is {}", opts.max_elements)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The frames a layered method can try, fewest levels first, or the first reason none fits.
|
||||
fn candidate_setups(ctx: &Ctx, method: HexMethod) -> std::result::Result<Vec<layered::Setup>, String> {
|
||||
let frames = if method == HexMethod::Extrude { layered::translations(ctx.brep) } else { layered::rotations(ctx.brep) };
|
||||
@@ -292,7 +307,7 @@ fn candidate_setups(ctx: &Ctx, method: HexMethod) -> std::result::Result<Vec<lay
|
||||
fn with_refinement(ctx: &Ctx, method: HexMethod, run: impl Fn(u32) -> Result<Raw>) -> Result<HexMesh> {
|
||||
let mut last = Error::Failed("no attempt".into());
|
||||
for refine in 0..=MAX_REFINE {
|
||||
match run(refine).and_then(|raw| finish(ctx, raw, method)) {
|
||||
match run(refine).and_then(|raw| finish(&Cells::one(ctx), raw, method)) {
|
||||
Ok(mesh) => return Ok(mesh),
|
||||
Err(e @ Error::Failed(_)) => last = e,
|
||||
Err(e) => return Err(e),
|
||||
@@ -314,14 +329,130 @@ fn hex_face(k: usize) -> [usize; 4] {
|
||||
ElementKind::Hex8.faces().get(k).and_then(|f| <[usize; 4]>::try_from(*f).ok()).unwrap_or([0, 1, 2, 3])
|
||||
}
|
||||
|
||||
/// Tag, check, upgrade to hex20 when asked, gate and measure.
|
||||
fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
/// The cells a mesh spans, for [`finish`]: one context per cell and the cell of each element.
|
||||
pub(crate) struct Cells<'c, 'a> {
|
||||
pub ctxs: &'c [Ctx<'a>],
|
||||
/// The cell of each hexahedron (index into `ctxs`); empty: all in the first.
|
||||
pub of: &'c [u32],
|
||||
/// The cells' names, for messages.
|
||||
pub names: &'c [&'c str],
|
||||
}
|
||||
|
||||
impl<'c, 'a> Cells<'c, 'a> {
|
||||
pub(crate) fn one(ctx: &'c Ctx<'a>) -> Self {
|
||||
Cells { ctxs: std::slice::from_ref(ctx), of: &[], names: &[] }
|
||||
}
|
||||
|
||||
fn name(&self, c: usize) -> String {
|
||||
self.names.get(c).map_or_else(|| c.to_string(), |n| format!("`{n}`"))
|
||||
}
|
||||
|
||||
fn cell(&self, e: usize) -> usize {
|
||||
self.of.get(e).map_or(0, |&c| c as usize)
|
||||
}
|
||||
|
||||
fn ctx(&self, e: usize) -> Result<&Ctx<'a>> {
|
||||
self.ctxs.get(self.cell(e)).ok_or_else(|| Error::Failed(format!("element {e} has no cell")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Buckets of a uniform grid over the cells' bounding boxes: which cells may be near a point.
|
||||
struct CellGrid {
|
||||
lo: P,
|
||||
step: f64,
|
||||
dims: [usize; 3],
|
||||
buckets: Vec<Vec<usize>>,
|
||||
}
|
||||
|
||||
/// Cosine of the largest angle between a boundary face's normal and the reversed normal of
|
||||
/// another cell's triangle for the face to lie on that triangle (about 25°): a face on another
|
||||
/// cell faces it, while faces that merely meet at an edge (a fillet beside a split) do not.
|
||||
const FACING: f64 = 0.9;
|
||||
|
||||
/// Buckets per axis of a [`CellGrid`] at most.
|
||||
const MAX_GRID_DIM: usize = 64;
|
||||
|
||||
impl CellGrid {
|
||||
/// The cells' boxes grown by `gap`.
|
||||
fn new(ctxs: &[Ctx], gap: f64) -> CellGrid {
|
||||
let boxes: Vec<(P, P)> = ctxs
|
||||
.iter()
|
||||
.map(|c| {
|
||||
c.brep.surface.points().iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(mut lo, mut hi), p| {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(p[k] - gap);
|
||||
hi[k] = hi[k].max(p[k] + gap);
|
||||
}
|
||||
(lo, hi)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let (lo, hi) = boxes.iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(mut lo, mut hi), (a, b)| {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(a[k]);
|
||||
hi[k] = hi[k].max(b[k]);
|
||||
}
|
||||
(lo, hi)
|
||||
});
|
||||
let per_axis = ((ctxs.len() as f64).cbrt().ceil() as usize * 2).clamp(1, MAX_GRID_DIM);
|
||||
let extent = (0..3).map(|k| hi[k] - lo[k]).fold(0.0, f64::max);
|
||||
let step = if extent > 0.0 && extent.is_finite() { extent / per_axis as f64 } else { 1.0 };
|
||||
let dim = |k: usize| (((hi[k] - lo[k]) / step).ceil() as usize).clamp(1, MAX_GRID_DIM);
|
||||
let dims = [dim(0), dim(1), dim(2)];
|
||||
let mut grid = CellGrid { lo, step, dims, buckets: vec![Vec::new(); dims[0] * dims[1] * dims[2]] };
|
||||
for (c, (a, b)) in boxes.iter().enumerate() {
|
||||
let (i0, i1) = (grid.index(*a), grid.index(*b));
|
||||
for i in i0[0]..=i1[0] {
|
||||
for j in i0[1]..=i1[1] {
|
||||
for k in i0[2]..=i1[2] {
|
||||
if let Some(bucket) = grid.buckets.get_mut((k * dims[1] + j) * dims[0] + i) {
|
||||
bucket.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
grid
|
||||
}
|
||||
|
||||
fn index(&self, p: P) -> [usize; 3] {
|
||||
[0, 1, 2].map(|k| {
|
||||
let x = ((p[k] - self.lo[k]) / self.step).floor();
|
||||
if x.is_finite() && x > 0.0 { (x as usize).min(self.dims[k] - 1) } else { 0 }
|
||||
})
|
||||
}
|
||||
|
||||
fn near(&self, p: P) -> &[usize] {
|
||||
let [i, j, k] = self.index(p);
|
||||
self.buckets.get((k * self.dims[1] + j) * self.dims[0] + i).map(Vec::as_slice).unwrap_or(&[])
|
||||
}
|
||||
}
|
||||
|
||||
/// A boundary quadrilateral of a mesh being finished: the element face, its cell and the B-rep
|
||||
/// face (index into the cell's faces) it lies on.
|
||||
struct Skin {
|
||||
element: u32,
|
||||
face: u8,
|
||||
cell: usize,
|
||||
brep: usize,
|
||||
}
|
||||
|
||||
/// Tag, check, upgrade to hex20 when asked, gate and measure. Besides the gate, the tiling and
|
||||
/// the fidelity checks, no boundary face of a mesh of several cells may lie on another cell's
|
||||
/// boundary (a free internal face: cells that do not conform there), and the mesh boundary must
|
||||
/// be a closed 2-manifold of quadrilaterals (every edge on exactly two).
|
||||
fn finish(cells: &Cells, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
let Raw { mut nodes, hexes, description } = raw;
|
||||
let first = cells.ctxs.first().ok_or_else(|| Error::Failed("no cell to finish".into()))?;
|
||||
let opts = first.opts;
|
||||
if hexes.is_empty() {
|
||||
return Err(Error::Failed("the method produced no elements".into()));
|
||||
}
|
||||
if hexes.len() > ctx.opts.max_elements {
|
||||
return Err(Error::Limit(format!("the mesh has {} hexahedra; the limit is {}", hexes.len(), ctx.opts.max_elements)));
|
||||
if hexes.len() > opts.max_elements {
|
||||
return Err(Error::Limit(format!("the mesh has {} hexahedra; the limit is {}", hexes.len(), opts.max_elements)));
|
||||
}
|
||||
if !cells.of.is_empty() && cells.of.len() != hexes.len() {
|
||||
return Err(Error::Failed("a cell is needed for every element".into()));
|
||||
}
|
||||
fieldcraft_model::check_coordinates(nodes.as_flattened())?;
|
||||
let at = |nodes: &[P], i: u32| nodes.get(i as usize).copied().ok_or_else(|| Error::Failed(format!("node {i} out of range")));
|
||||
@@ -337,8 +468,14 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
if let Some(n) = uses.values().copied().max().filter(|&n| n > 2) {
|
||||
return Err(Error::Failed(format!("a hexahedron face is shared by {n} elements (the mesh is not a manifold)")));
|
||||
}
|
||||
// A boundary face this close to another cell's boundary lies on it.
|
||||
let gap = 2.0 * opts.chordal_tolerance + cells.ctxs.iter().map(|c| c.tol).fold(0.0, f64::max);
|
||||
let others = (cells.ctxs.len() > 1).then(|| CellGrid::new(cells.ctxs, gap));
|
||||
let mut skin: Vec<Skin> = Vec::new();
|
||||
let mut boundary = Vec::new();
|
||||
let mut fidelity = 0.0;
|
||||
// Boundary edges of the quadrilaterals: each must be on exactly two of them.
|
||||
let mut rim: HashMap<(u32, u32), u32> = HashMap::new();
|
||||
for (e, h) in hexes.iter().enumerate() {
|
||||
for k in 0..6 {
|
||||
let corners = hex_face(k).map(|i| h.get(i).copied().unwrap_or(0));
|
||||
@@ -347,6 +484,11 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
if uses.get(&key) != Some(&1) {
|
||||
continue;
|
||||
}
|
||||
for m in 0..4 {
|
||||
let (a, b) = (corners[m], corners[(m + 1) % 4]);
|
||||
*rim.entry((a.min(b), a.max(b))).or_insert(0) += 1;
|
||||
}
|
||||
let ctx = cells.ctx(e)?;
|
||||
let [a, b, c, d] = [at(&nodes, corners[0])?, at(&nodes, corners[1])?, at(&nodes, corners[2])?, at(&nodes, corners[3])?];
|
||||
let centre = scale(add(add(a, b), add(c, d)), 0.25);
|
||||
let n = cross(sub(c, a), sub(d, b));
|
||||
@@ -357,14 +499,35 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
.closest(centre, &|t| dot(ctx.tree.normal(t), n) > 0.0)
|
||||
.ok_or_else(|| Error::Failed(format!("no boundary face faces the same way as element {e}'s face {}", k + 1)))?;
|
||||
let f = ctx.brep.face_of_triangle(hit.triangle).ok_or_else(|| Error::Failed("triangle without a face".into()))?;
|
||||
fidelity += area * (hit.dist + ctx.opts.chordal_tolerance);
|
||||
boundary.push(BoundaryFace { element: e as u32, face: k as u8 + 1, brep_face: ctx.brep.face_tag(f) });
|
||||
let cell = cells.cell(e);
|
||||
if let Some(grid) = &others {
|
||||
for &o in grid.near(centre) {
|
||||
let Some(other) = cells.ctxs.get(o).filter(|_| o != cell) else { continue };
|
||||
if other.tree.closest_within(centre, gap, &|t| dot(other.tree.normal(t), n) < -FACING).is_some() {
|
||||
return Err(Error::Failed(format!(
|
||||
"element {e}'s face {} lies on cell {}'s boundary without a neighbour there: cells {} and {} do not conform (split them so that they meet face to face)",
|
||||
k + 1,
|
||||
cells.name(o),
|
||||
cells.name(cell),
|
||||
cells.name(o)
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// The geometry's own face tag (a member of a composite face keeps its own).
|
||||
let tag = ctx.brep.surface.tri_face().get(hit.triangle as usize).copied().unwrap_or_else(|| ctx.brep.face_tag(f));
|
||||
fidelity += area * (hit.dist + opts.chordal_tolerance);
|
||||
boundary.push(BoundaryFace { element: e as u32, face: k as u8 + 1, brep_face: tag });
|
||||
skin.push(Skin { element: e as u32, face: k as u8 + 1, cell, brep: f });
|
||||
}
|
||||
}
|
||||
let kind = if ctx.opts.quadratic { ElementKind::Hex20 } else { ElementKind::Hex8 };
|
||||
if let Some((&(a, b), &n)) = rim.iter().filter(|(_, n)| **n != 2).min_by_key(|(k, _)| **k) {
|
||||
return Err(Error::Failed(format!("the mesh boundary is not closed: the edge between nodes {a} and {b} is on {n} boundary faces")));
|
||||
}
|
||||
let kind = if opts.quadratic { ElementKind::Hex20 } else { ElementKind::Hex8 };
|
||||
let mut connectivity: Vec<u32> = Vec::with_capacity(hexes.len() * kind.node_count());
|
||||
if ctx.opts.quadratic {
|
||||
let mids = mid_edge_nodes(ctx, &mut nodes, &hexes, &boundary)?;
|
||||
if opts.quadratic {
|
||||
let mids = mid_edge_nodes(cells, &mut nodes, &hexes, &skin)?;
|
||||
for (h, m) in hexes.iter().zip(&mids) {
|
||||
connectivity.extend_from_slice(h);
|
||||
connectivity.extend_from_slice(m);
|
||||
@@ -398,7 +561,7 @@ fn finish(ctx: &Ctx, raw: Raw, method: HexMethod) -> Result<HexMesh> {
|
||||
let gate = validity_gate_checks(checks.iter().map(|(l, c)| (*l, c)), &GateOptions { expected_volume: Some(enclosed), volume_tolerance: 1e-9 })?
|
||||
.into_result()
|
||||
.map_err(|e| Error::Failed(format!("{} ({description})", plain(&e))))?;
|
||||
let surface_volume = ctx.brep.surface.volume();
|
||||
let surface_volume: f64 = cells.ctxs.iter().map(|c| c.brep.surface.volume()).sum();
|
||||
let slack = 2.0 * fidelity + 1e-9 * surface_volume.abs();
|
||||
if (gate.volume - surface_volume).abs() > slack {
|
||||
return Err(Error::Failed(format!(
|
||||
@@ -478,46 +641,52 @@ pub(crate) fn project_to_face(ctx: &Ctx, f: usize, p: P) -> Option<P> {
|
||||
return Some(q);
|
||||
}
|
||||
let tag = face.id;
|
||||
let tri_face = ctx.brep.surface.tri_face();
|
||||
ctx.tree.closest(p, &|t| tri_face.get(t as usize) == Some(&tag)).map(|h| h.point)
|
||||
ctx.tree.closest(p, &|t| ctx.brep.tag(t) == Some(tag)).map(|h| h.point)
|
||||
}
|
||||
|
||||
/// The point on every listed face (their common curve or corner) nearest `p`, by alternating
|
||||
/// projections.
|
||||
pub(crate) fn project_to_faces(ctx: &Ctx, faces: &[usize], p: P) -> P {
|
||||
let pairs: Vec<(&Ctx, usize)> = faces.iter().map(|&f| (ctx, f)).collect();
|
||||
project_alternating(&pairs, ctx.tol, p)
|
||||
}
|
||||
|
||||
/// The point on every listed face of the listed cells nearest `p`, by alternating projections
|
||||
/// until a round moves it less than a thousandth of `tol`.
|
||||
fn project_alternating(faces: &[(&Ctx, usize)], tol: f64, p: P) -> P {
|
||||
let mut x = p;
|
||||
for _ in 0..PROJECTION_ROUNDS {
|
||||
let before = x;
|
||||
for &f in faces {
|
||||
for &(ctx, f) in faces {
|
||||
if let Some(q) = project_to_face(ctx, f, x) {
|
||||
x = q;
|
||||
}
|
||||
}
|
||||
if faces.len() < 2 || dist(before, x) <= ctx.tol * 1e-3 {
|
||||
if faces.len() < 2 || dist(before, x) <= tol * 1e-3 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// Mid-edge nodes of every hexahedron (Hex20 order: positions 8..20), shared between elements;
|
||||
/// boundary edges get theirs on the B-rep faces of the boundary quads around them.
|
||||
fn mid_edge_nodes(ctx: &Ctx, nodes: &mut Vec<P>, hexes: &[[u32; 8]], boundary: &[BoundaryFace]) -> Result<Vec<[u32; 12]>> {
|
||||
// B-rep faces at each boundary edge.
|
||||
let mut edge_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
|
||||
for b in boundary {
|
||||
/// Mid-edge nodes of every hexahedron (Hex20 order: positions 8..20), shared between elements
|
||||
/// (of any cell); boundary edges get theirs on the B-rep faces of the boundary quads around them.
|
||||
fn mid_edge_nodes(cells: &Cells, nodes: &mut Vec<P>, hexes: &[[u32; 8]], skin: &[Skin]) -> Result<Vec<[u32; 12]>> {
|
||||
// B-rep faces (cell, face) at each boundary edge.
|
||||
let mut edge_faces: HashMap<(u32, u32), Vec<(usize, usize)>> = HashMap::new();
|
||||
for b in skin {
|
||||
let Some(h) = hexes.get(b.element as usize) else { continue };
|
||||
let c = hex_face(b.face as usize - 1).map(|i| h.get(i).copied().unwrap_or(0));
|
||||
let f = ctx.brep.faces.binary_search_by_key(&b.brep_face, |x| x.id).map_err(|_| Error::Failed("boundary tag".into()))?;
|
||||
for k in 0..4 {
|
||||
let (a, z) = (c[k], c[(k + 1) % 4]);
|
||||
let list = edge_faces.entry((a.min(z), a.max(z))).or_default();
|
||||
if !list.contains(&f) {
|
||||
list.push(f);
|
||||
if !list.contains(&(b.cell, b.brep)) {
|
||||
list.push((b.cell, b.brep));
|
||||
}
|
||||
}
|
||||
}
|
||||
let edges = ElementKind::Hex20.edges();
|
||||
let tol = cells.ctxs.iter().map(|c| c.tol).fold(f64::INFINITY, f64::min);
|
||||
let mut mid: HashMap<(u32, u32), u32> = HashMap::new();
|
||||
let mut out = Vec::with_capacity(hexes.len());
|
||||
for h in hexes {
|
||||
@@ -533,7 +702,10 @@ fn mid_edge_nodes(ctx: &Ctx, nodes: &mut Vec<P>, hexes: &[[u32; 8]], boundary: &
|
||||
let (a, b) = (nodes.get(i as usize).copied().unwrap_or([0.0; 3]), nodes.get(j as usize).copied().unwrap_or([0.0; 3]));
|
||||
let p = scale(add(a, b), 0.5);
|
||||
let p = match edge_faces.get(&key) {
|
||||
Some(fs) => project_to_faces(ctx, fs, p),
|
||||
Some(fs) => {
|
||||
let pairs: Vec<(&Ctx, usize)> = fs.iter().filter_map(|&(c, f)| cells.ctxs.get(c).map(|ctx| (ctx, f))).collect();
|
||||
project_alternating(&pairs, tol, p)
|
||||
}
|
||||
None => p,
|
||||
};
|
||||
nodes.push(p);
|
||||
|
||||
Reference in new issue
Block a user