mesh.block: 2D patches (quad4, quad8, tri3, tri6) with arc and ellipse edges

A four-sided planar patch at z = 0, meshed by the Coons transfinite
interpolation of its edges for plane stress, plane strain and axisymmetric
sections: the elements fill a named face of the body, edges and corners
carry names, patches join edge to edge (the join check treats the edges of
2D elements as their sides).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-13 21:42:19 -04:00
1 parent ca02c01e1f
commit 84e81acf3a
3 files changed
+336 -21

No files matched your search

+105 -17
View File
@@ -1,10 +1,11 @@
//! `mesh.block`: a structured mesh of one six-sided block from its corners and curved edges
//! ([`modelcraft_mesh::block`]), joined to the mesh already there and tagged as a body with named
//! faces, edges and vertices, so sets by geometry select it.
//! `mesh.block`: a structured mesh of one six-sided block (solid elements) or four-sided planar
//! patch (2D elements) from its corners and curved edges ([`modelcraft_mesh::block`]), joined to
//! the mesh already there and tagged as a body (a patch: a face of the body) with named faces,
//! edges and vertices, so sets by geometry select it.
use std::collections::{BTreeMap, BTreeSet};
use modelcraft_mesh::block::{Block, Curve, block};
use modelcraft_mesh::block::{Block, BlockMesh, Curve, Patch, block, patch};
use modelcraft_mesh::join::{check_conforming, weld};
use modelcraft_model::{ElementKind, Entity, GeometryRef, Model};
use serde::Deserialize;
@@ -16,7 +17,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
pub const BLOCK: CommandSpec = CommandSpec {
id: "mesh.block",
label: "Mesh Block",
params: r#"{"body": "Plate", "kind": "hex8|hex20|tet4|tet10|wedge6", "corners": [[x, y, z] × 8] (hex8 order: 1-2-3-4 counter-clockwise seen from 5-6-7-8), "divisions": [n12, n14, n15] (elements along 1→2, 1→4, 1→5), "bias": [1, 1, 1] (optional: last over first element size along each direction), "edges": [{"corners": [1, 2], "arc": {"center": [x, y, z]}} | {"corners": [2, 3], "ellipse": {"center": [x, y, z], "axes": [[a], [b]] (perpendicular)}} | {"corners": [5, 6], "name": "tip_edge"}] (optional: curved edges, the shorter arc between the corners, and edge names), "faces": {"1": "bottom", "4": "tip"} (optional: names of block faces, numbered as hex8 faces: 1 = 1-2-3-4, 2 = 5-6-7-8, 3 = 1-2-6-5, 4 = 2-3-7-6, 5 = 3-4-8-7, 6 = 4-1-5-8), "vertices": {"1": "A"} (optional: names of corners), "merge": distance (optional, default 1e-6 of the block's diagonal; 0 never joins)} — adds a mapped mesh of the block (transfinite interpolation of its edges; quadratic nodes on the curves), uses existing nodes within `merge` of new ones so blocks that share a face join (the shared face must have the same elements on both sides, or nothing changes), and tags the elements with the body and element faces, edges and nodes with the named faces, edges and vertices"#,
params: r#"{"body": "Plate", "kind": "hex8|hex20|tet4|tet10|wedge6", "corners": [[x, y, z] × 8] (hex8 order: 1-2-3-4 counter-clockwise seen from 5-6-7-8), "divisions": [n12, n14, n15] (elements along 1→2, 1→4, 1→5), "bias": [1, 1, 1] (optional: last over first element size along each direction), "edges": [{"corners": [1, 2], "arc": {"center": [x, y, z]}} | {"corners": [2, 3], "ellipse": {"center": [x, y, z], "axes": [[a], [b]] (perpendicular)}} | {"corners": [5, 6], "name": "tip_edge"}] (optional: curved edges, the shorter arc between the corners, and edge names), "faces": {"1": "bottom", "4": "tip"} (optional: names of block faces, numbered as hex8 faces: 1 = 1-2-3-4, 2 = 5-6-7-8, 3 = 1-2-6-5, 4 = 2-3-7-6, 5 = 3-4-8-7, 6 = 4-1-5-8), "vertices": {"1": "A"} (optional: names of corners), "merge": distance (optional, default 1e-6 of the block's diagonal; 0 never joins)} — adds a mapped mesh of the block (transfinite interpolation of its edges; quadratic nodes on the curves), uses existing nodes within `merge` of new ones so blocks that share a face join (the shared face must have the same elements on both sides, or nothing changes), and tags the elements with the body and element faces, edges and nodes with the named faces, edges and vertices. 2D: "kind": "quad4|quad8|tri3|tri6", four "corners" at z = 0 (counter-clockwise from +z), "divisions": [n12, n14], "bias": [r, r], "face": "area" (the face of the body the elements fill, default `area`), edges and vertices numbered among the four corners; for plane stress, plane strain and axisymmetric sections"#,
kind: CommandKind::Model,
run: block_mesh,
};
@@ -26,21 +27,22 @@ pub const BLOCK: CommandSpec = CommandSpec {
struct Params {
body: String,
kind: String,
corners: [[f64; 3]; 8],
divisions: [usize; 3],
#[serde(default = "uniform")]
bias: [f64; 3],
corners: Vec<[f64; 3]>,
divisions: Vec<usize>,
bias: Option<Vec<f64>>,
#[serde(default)]
edges: Vec<EdgeParams>,
#[serde(default)]
faces: BTreeMap<String, String>,
face: Option<String>,
#[serde(default)]
vertices: BTreeMap<String, String>,
merge: Option<f64>,
}
fn uniform() -> [f64; 3] {
[1.0; 3]
/// A list parameter of exactly `N` entries.
fn exactly<T: Copy, const N: usize>(v: &[T], what: &str, dimension: u8) -> Result<[T; N]> {
<[T; N]>::try_from(v).map_err(|_| EngineError::Params(format!("a {dimension}D block needs {N} {what}, got {}", v.len())))
}
#[derive(Deserialize)]
@@ -77,12 +79,14 @@ fn numbered(key: &str, n: usize, what: &str) -> Result<usize> {
fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
let p: Params = serde_json::from_value(p.clone()).map_err(|e| EngineError::Params(format!("mesh.block: {e}")))?;
let kind = ElementKind::from_name(&p.kind).ok_or_else(|| EngineError::Params(format!("unknown element kind `{}`", p.kind)))?;
let planar = kind.dimension() == 2;
let (corner_count, dimension) = if planar { (4, 2) } else { (8, 3) };
let edge_index = |pair: [usize; 2]| if planar { Patch::edge_index(pair) } else { Block::edge_index(pair) };
let mut curves = Vec::new();
let mut edge_names = Vec::new();
for e in &p.edges {
let pair = [0, 1].map(|i| e.corners[i].wrapping_sub(1));
let index =
Block::edge_index(pair).ok_or_else(|| EngineError::Params(format!("corners {:?} are not the two ends of a block edge", e.corners)))?;
let index = edge_index(pair).ok_or_else(|| EngineError::Params(format!("corners {:?} are not the two ends of an edge", e.corners)))?;
let curve = match (&e.arc, &e.ellipse) {
(Some(a), None) => Some(Curve::Arc { center: a.center }),
(None, Some(el)) => Some(Curve::Ellipse { center: el.center, axes: el.axes }),
@@ -100,16 +104,42 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
}
}
// Check every name before meshing.
let body = GeometryRef::body(p.body.clone())?;
let region = match (planar, &p.face) {
(true, face) => GeometryRef::face(p.body.clone(), face.clone().unwrap_or_else(|| "area".into()))?,
(false, None) => GeometryRef::body(p.body.clone())?,
(false, Some(_)) => return Err(EngineError::Params("`face` names the region of 2D elements; a solid block fills its body".into())),
};
if planar && !p.faces.is_empty() {
return Err(EngineError::Params("a 2D patch has no faces to name: name its edges".into()));
}
let named = |entries: &BTreeMap<String, String>, n: usize, what: &str, entity: fn(String) -> Entity| -> Result<Vec<(usize, GeometryRef)>> {
entries.iter().map(|(k, name)| Ok((numbered(k, n, what)?, GeometryRef::new(p.body.clone(), entity(name.clone()))?))).collect()
};
let face_refs = named(&p.faces, 6, "face", Entity::Face)?;
let vertex_refs = named(&p.vertices, 8, "vertex", Entity::Vertex)?;
let vertex_refs = named(&p.vertices, corner_count, "vertex", Entity::Vertex)?;
let edge_refs: Vec<(usize, GeometryRef)> =
edge_names.into_iter().map(|(k, n)| Ok((k, GeometryRef::new(p.body.clone(), Entity::Edge(n))?))).collect::<Result<_>>()?;
let mesh = block(&Block { kind, corners: p.corners, divisions: p.divisions, bias: p.bias, curves })?;
let bias = p.bias.clone().unwrap_or_else(|| vec![1.0; dimension as usize]);
let mesh: BlockMesh = if planar {
let corners = exactly(&p.corners, "corners", dimension)?;
patch(&Patch {
kind,
corners,
divisions: exactly(&p.divisions, "divisions", dimension)?,
bias: exactly(&bias, "bias values", dimension)?,
curves,
})?
} else {
let corners = exactly(&p.corners, "corners", dimension)?;
block(&Block {
kind,
corners,
divisions: exactly(&p.divisions, "divisions", dimension)?,
bias: exactly(&bias, "bias values", dimension)?,
curves,
})?
};
let diagonal = {
let lo = (0..3).map(|d| p.corners.iter().map(|c| c[d]).fold(f64::INFINITY, f64::min));
let hi = (0..3).map(|d| p.corners.iter().map(|c| c[d]).fold(f64::NEG_INFINITY, f64::max));
@@ -142,7 +172,7 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
mesh.elements.iter().map(|e| Ok((kind, e.iter().map(|&i| label_of(i)).collect::<Result<_>>()?))).collect::<Result<_>>()?;
check_conforming(&s.model, &elements, &joined, merge)?;
let body_tag = s.model.geometry_tag(&body)?;
let body_tag = s.model.geometry_tag(&region)?;
for (label, (_, nodes)) in (first_element..).zip(elements) {
s.model.set_element(label, kind, nodes)?;
s.model.tag_element(label, body_tag)?;
@@ -294,4 +324,62 @@ mod tests {
}
assert_eq!(s.journal.len(), journal);
}
#[test]
fn patches_mesh_2d_regions_that_join_and_solve() {
let mut s = Session::default();
let o = [0, 0, 0];
// Two quarter annuli of quad8 (a = 1, b = 2), side by side: a half annulus.
for (corners, names, vertices) in [
(
json!([[1, 0, 0], [2, 0, 0], [0, 2, 0], [0, 1, 0]]),
json!([{"corners": [1, 2], "name": "y0"}, {"corners": [1, 4], "arc": {"center": o}, "name": "bore"}]),
json!({"3": "top"}),
),
(
json!([[0, 1, 0], [0, 2, 0], [-2, 0, 0], [-1, 0, 0]]),
json!([{"corners": [1, 4], "arc": {"center": o}, "name": "bore"}, {"corners": [3, 4], "name": "y0"}]),
json!({}),
),
] {
let mut edges = names.as_array().unwrap().clone();
edges.push(json!({"corners": [2, 3], "arc": {"center": o}}));
s.execute(
"mesh.block",
&json!({"body": "Ring", "kind": "quad8", "corners": corners, "divisions": [4, 8], "edges": edges, "vertices": vertices}),
)
.unwrap();
}
assert_eq!(s.model.elements().len(), 64);
for (cmd, params) in [
("model.set.create", json!({"name": "all", "geometry": [{"body": "Ring", "face": "area"}]})),
("model.set.create", json!({"name": "y0", "geometry": [{"body": "Ring", "edge": "y0"}]})),
("model.set.create", json!({"name": "bore", "geometry": [{"body": "Ring", "edge": "bore"}]})),
("model.set.create", json!({"name": "top", "geometry": [{"body": "Ring", "vertex": "top"}]})),
("model.boundary_condition.create", json!({"name": "hold", "type": "symmetry", "target": {"set": "top"}, "plane": "x"})),
("model.material.create", json!({"name": "Steel", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}})),
("model.section.create", json!({"name": "ring", "set": "all", "material": "Steel", "kind": {"type": "plane_strain", "thickness": 1.0}})),
("model.boundary_condition.create", json!({"name": "sym", "type": "symmetry", "target": {"set": "y0"}, "plane": "y"})),
("model.load.create", json!({"name": "p", "type": "pressure", "set": "bore", "magnitude": 10.0})),
("model.step.create", json!({"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["sym", "hold"], "loads": ["p"]})),
] {
s.execute(cmd, &params).unwrap_or_else(|e| panic!("{cmd}: {e}"));
}
s.execute("solve.static", &json!({})).unwrap();
// Lamé, plane strain: u_r(a) = (1 + ν) a² p / (E (b² − a²)) ((1 − 2ν) a + b²/a).
let exact = 1.3 * 10.0 / (1000.0 * 3.0) * (0.4 + 4.0);
let r = s.execute("solve.probe", &json!({"step": "Load", "at": [[0, 1, 0]]})).unwrap();
let u = r["nodes"][0]["displacement"][1].as_f64().unwrap();
assert!((u - exact).abs() < 2e-3 * exact, "{u} vs {exact}");
// 2D patches refuse solid parameters, and the other way round.
for bad in [
json!({"body": "P", "kind": "quad4", "corners": [[5, 0, 0], [6, 0, 0], [6, 1, 0], [5, 1, 0]], "divisions": [1, 1, 1]}),
json!({"body": "P", "kind": "quad4", "corners": [[5, 0, 0], [6, 0, 0], [6, 1, 0], [5, 1, 0]], "divisions": [1, 1], "faces": {"1": "x"}}),
json!({"body": "P", "kind": "quad4", "corners": [[5, 0, 1], [6, 0, 1], [6, 1, 1], [5, 1, 1]], "divisions": [1, 1]}),
json!({"body": "P", "kind": "hex8", "corners": [[5, 0, 0], [6, 0, 0], [6, 1, 0], [5, 1, 0]], "divisions": [1, 1]}),
json!({"body": "P", "kind": "hex8", "corners": cube(5.0), "divisions": [1, 1, 1], "face": "area"}),
] {
assert!(s.execute("mesh.block", &bad).is_err(), "{bad}");
}
}
}
+226 -2
View File
@@ -18,7 +18,7 @@
use modelcraft_model::{ElementKind, MAX_COORD};
use crate::quality::{Validity, element_validity};
use crate::quality::{Validity, element_validity, planar_quad_validity};
use crate::vec3::{P, cross, dot, norm, sub};
use crate::{Error, Result};
@@ -76,7 +76,7 @@ pub struct BlockMesh {
pub faces: Vec<(u32, u8, u8)>,
/// `(element, edge 1-based, block edge 0..12)`: block edges as in [`EDGES`].
pub edges: Vec<(u32, u8, u8)>,
/// The point at each block corner.
/// The point at each block corner (a patch fills the first four).
pub corners: [u32; 8],
}
@@ -401,6 +401,184 @@ pub fn block(b: &Block) -> Result<BlockMesh> {
Ok(BlockMesh { points, elements, faces, edges, corners })
}
/// The corners of a patch's four edges, as [`EDGES`] for blocks: two along 1→2 (at the start
/// and the end of 1→4), then two along 1→4.
pub const PATCH_EDGES: [[usize; 2]; 4] = [[0, 1], [3, 2], [0, 3], [1, 2]];
/// One four-sided planar patch to mesh with 2D elements (plane stress, plane strain and
/// axisymmetric sections live in the XY plane, so its corners and curves are at z = 0).
#[derive(Debug, Clone, PartialEq)]
pub struct Patch {
/// quad4, quad8, tri3 or tri6.
pub kind: ElementKind,
/// The corners counter-clockwise seen from +z.
pub corners: [P; 4],
/// Elements along 1→2 and 1→4.
pub divisions: [usize; 2],
pub bias: [f64; 2],
pub curves: Vec<([usize; 2], Curve)>,
}
impl Patch {
/// The patch edge ([`PATCH_EDGES`] index) joining two 0-based corners, either way round.
pub fn edge_index(corners: [usize; 2]) -> Option<usize> {
PATCH_EDGES.iter().position(|e| *e == corners || *e == [corners[1], corners[0]])
}
}
/// Lattice offsets of each 2D element of one cell, in the model's node order.
fn cell_elements_2d(kind: ElementKind) -> Vec<Vec<[usize; 2]>> {
let nodes = |corners: &[[usize; 2]]| -> Vec<[usize; 2]> {
if !kind.is_quadratic() {
return corners.to_vec();
}
let mut out: Vec<[usize; 2]> = corners.iter().map(|c| c.map(|x| 2 * x)).collect();
for edge in kind.edges() {
if let (Some(a), Some(b)) = (edge.first().and_then(|&i| corners.get(i)), edge.get(1).and_then(|&i| corners.get(i))) {
out.push([a[0] + b[0], a[1] + b[1]]);
}
}
out
};
match kind {
ElementKind::Quad4 | ElementKind::Quad8 => vec![nodes(&[[0, 0], [1, 0], [1, 1], [0, 1]])],
ElementKind::Tri3 | ElementKind::Tri6 => vec![nodes(&[[0, 0], [1, 0], [1, 1]]), nodes(&[[0, 0], [1, 1], [0, 1]])],
_ => Vec::new(),
}
}
/// Mesh one patch with 2D elements by the Coons (bilinearly blended) transfinite interpolation of
/// its four edges. Each element's corners must turn counter-clockwise (exact predicates); mid-side
/// nodes come from the map, so quadratic elements follow the curved edges. In the result,
/// `faces` is empty and `edges` lists element edges on the patch edges ([`PATCH_EDGES`]).
pub fn patch(b: &Patch) -> Result<BlockMesh> {
let kind = b.kind;
if !matches!(kind, ElementKind::Quad4 | ElementKind::Quad8 | ElementKind::Tri3 | ElementKind::Tri6) {
return Err(Error::Invalid(format!("a patch is meshed with quad4, quad8, tri3 or tri6 elements, not {}", kind.name())));
}
if b.corners.iter().flatten().any(|c| !c.is_finite() || c.abs() > MAX_COORD) {
return Err(Error::Invalid(format!("patch corners must be finite and within ±{MAX_COORD}")));
}
if b.corners.iter().any(|c| c[2] != 0.0) {
return Err(Error::Invalid("2D elements lie in the XY plane: give the patch corners with z = 0".into()));
}
if b.divisions.iter().any(|&n| n == 0 || n > MAX_DIVISIONS) {
return Err(Error::Invalid(format!("divisions must be 1 to {MAX_DIVISIONS} in each direction")));
}
if b.bias.iter().any(|&r| !(r.is_finite() && (1e-6..=1e6).contains(&r))) {
return Err(Error::Invalid("bias must be between 1e-6 and 1e6 (1 is uniform)".into()));
}
let pattern = cell_elements_2d(kind);
b.divisions[0]
.checked_mul(b.divisions[1])
.and_then(|c| c.checked_mul(pattern.len()))
.filter(|&n| n <= MAX_BLOCK_ELEMENTS)
.ok_or_else(|| Error::Limit(format!("a patch makes at most {MAX_BLOCK_ELEMENTS} elements; {:?} divisions are more", b.divisions)))?;
let mut curves: [Option<Curve>; 4] = [None; 4];
for (pair, curve) in &b.curves {
let k = Patch::edge_index(*pair)
.ok_or_else(|| Error::Invalid(format!("corners {} and {} are not joined by a patch edge", pair[0] + 1, pair[1] + 1)))?;
if let Curve::Arc { center } | Curve::Ellipse { center, .. } = curve
&& center[2] != 0.0
{
return Err(Error::Invalid("a patch's curves lie in the XY plane: give their centres with z = 0".into()));
}
if curves[k].replace(*curve).is_some() {
return Err(Error::Invalid(format!("the edge from corner {} to {} is given twice", pair[0] + 1, pair[1] + 1)));
}
}
let order = if kind.is_quadratic() { 2 } else { 1 };
let params: [Vec<f64>; 2] = [0, 1].map(|d| parameters(b.divisions[d], b.bias[d], order));
let mut edges: [Vec<P>; 4] = Default::default();
for (k, [from, to]) in PATCH_EDGES.iter().enumerate() {
let path = Path::new(curves[k].unwrap_or(Curve::Line), b.corners[*from], b.corners[*to])?;
edges[k] = params[k / 2].iter().map(|&t| path.point(t)).collect();
}
let at = |i: usize, j: usize| -> P {
let (u, v) = (params[0].get(i).copied().unwrap_or(0.0), params[1].get(j).copied().unwrap_or(0.0));
let e = |k: usize, n: usize| edges[k].get(n).copied().unwrap_or([f64::NAN; 3]);
let [c0, c1, c2, c3] = b.corners;
let mut x = [0.0; 3];
for (f, p) in [(1.0 - v, e(0, i)), (v, e(1, i)), (1.0 - u, e(2, j)), (u, e(3, j))] {
x = add_scaled(x, f, p);
}
for (f, c) in [((1.0 - u) * (1.0 - v), c0), (u * (1.0 - v), c1), (u * v, c2), ((1.0 - u) * v, c3)] {
x = add_scaled(x, -f, c);
}
x[2] = 0.0;
x
};
let size = [params[0].len(), params[1].len()];
let mut used = vec![false; size[0] * size[1]];
let mut lattice: Vec<Vec<[usize; 2]>> = Vec::new();
for j in 0..b.divisions[1] {
for i in 0..b.divisions[0] {
for e in &pattern {
let nodes: Vec<[usize; 2]> = e.iter().map(|o| [i * order + o[0], j * order + o[1]]).collect();
for c in &nodes {
if let Some(slot) = used.get_mut(c[0] + size[0] * c[1]) {
*slot = true;
}
}
lattice.push(nodes);
}
}
}
let mut number: Vec<Option<u32>> = vec![None; used.len()];
let mut points = Vec::new();
for j in 0..size[1] {
for i in 0..size[0] {
let k = i + size[0] * j;
if used.get(k) == Some(&true)
&& let Some(slot) = number.get_mut(k)
{
*slot = Some(u32::try_from(points.len()).map_err(|_| Error::Limit("too many points in one patch".into()))?);
points.push(at(i, j));
}
}
}
let point_of = |c: [usize; 2]| number.get(c[0] + size[0] * c[1]).copied().flatten();
let last = [size[0] - 1, size[1] - 1];
let mut corners = [0u32; 8];
for (slot, c) in corners.iter_mut().zip([[0, 0], [last[0], 0], [last[0], last[1]], [0, last[1]]]) {
*slot = point_of(c).ok_or_else(|| Error::Failed("a patch corner has no point".into()))?;
}
// Each patch edge: the direction it runs along and the lattice coordinate fixed on it.
let patch_edges: [(usize, usize); 4] = [(0, 0), (0, last[1]), (1, 0), (1, last[0])];
let mut elements = Vec::with_capacity(lattice.len());
let mut on_edges = Vec::new();
for (e, nodes) in lattice.iter().enumerate() {
let label = u32::try_from(e).map_err(|_| Error::Limit("too many elements in one patch".into()))?;
let ids: Vec<u32> =
nodes.iter().map(|c| point_of(*c)).collect::<Option<_>>().ok_or_else(|| Error::Failed("an element node has no point".into()))?;
let xy: Vec<[f64; 2]> =
ids.iter().take(kind.corner_count()).map(|&i| points.get(i as usize).map_or([f64::NAN; 2], |p| [p[0], p[1]])).collect();
let valid = match xy.as_slice() {
[a, b, c, d] => planar_quad_validity(&[*a, *b, *c, *d])? == Validity::Valid,
[a, b, c] => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]) > 0.0,
_ => false,
};
if !valid {
let cell = nodes.first().map(|c| [c[0] / order, c[1] / order]).unwrap_or_default();
return Err(Error::Invalid(format!(
"the patch's {} element in cell {cell:?} is inverted or degenerate: give the corners counter-clockwise seen from +z and curves that do not fold the patch",
kind.name()
)));
}
for (k, local) in kind.edges().iter().enumerate() {
let (Some(a), Some(c)) = (local.first().and_then(|&i| nodes.get(i)), local.get(1).and_then(|&i| nodes.get(i))) else { continue };
for (pe, (along, fixed)) in patch_edges.iter().enumerate() {
let other = 1 - along;
if a[other] == *fixed && c[other] == *fixed {
on_edges.push((label, k as u8 + 1, pe as u8));
}
}
}
elements.push(ids);
}
Ok(BlockMesh { points, elements, faces: Vec::new(), edges: on_edges, corners })
}
#[cfg(test)]
mod tests {
use super::*;
@@ -496,6 +674,52 @@ mod tests {
assert!(inner.iter().all(|&i| (m.points[i as usize][0].hypot(m.points[i as usize][1]) - a).abs() < 1e-12));
}
#[test]
fn patches_mesh_a_quarter_annulus_with_every_2d_kind() {
let (a, b) = (1.0, 2.0);
let o = [0.0; 3];
for (kind, per_cell) in [(ElementKind::Quad4, 1), (ElementKind::Quad8, 1), (ElementKind::Tri3, 2), (ElementKind::Tri6, 2)] {
let p = Patch {
kind,
corners: [[a, 0.0, 0.0], [b, 0.0, 0.0], [0.0, b, 0.0], [0.0, a, 0.0]],
divisions: [3, 6],
bias: [1.0; 2],
curves: vec![([1, 2], Curve::Arc { center: o }), ([0, 3], Curve::Arc { center: o })],
};
let m = patch(&p).unwrap();
assert_eq!(m.elements.len(), 18 * per_cell, "{kind:?}");
assert!(m.points.iter().all(|q| q[2] == 0.0 && q[0].hypot(q[1]) > a - 1e-12 && q[0].hypot(q[1]) < b + 1e-12));
// Element edges on each patch edge: 3, 3, 6, 6 segments.
for (pe, n) in [(0, 3), (1, 3), (2, 6), (3, 6)] {
assert_eq!(m.edges.iter().filter(|e| e.2 == pe).count(), n, "{kind:?} edge {pe}");
}
if kind.is_quadratic() {
// Mid-side nodes of the inner arc lie on it.
let on_inner = m.edges.iter().filter(|e| e.2 == 2).all(|&(e, k, _)| {
let mid = kind.edges()[k as usize - 1][2];
let q = m.points[m.elements[e as usize][mid] as usize];
(q[0].hypot(q[1]) - a).abs() < 1e-12
});
assert!(on_inner, "{kind:?}");
}
}
let mut clockwise = Patch {
kind: ElementKind::Quad4,
corners: [[0.0; 3], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [1.0, 0.0, 0.0]],
divisions: [1, 1],
bias: [1.0; 2],
curves: vec![],
};
assert!(patch(&clockwise).is_err());
clockwise.corners.swap(1, 3);
assert!(patch(&clockwise).is_ok());
clockwise.corners[0][2] = 1.0;
assert!(patch(&clockwise).is_err(), "off the XY plane");
clockwise.corners[0][2] = 0.0;
clockwise.kind = ElementKind::Hex8;
assert!(patch(&clockwise).is_err());
}
#[test]
fn grading_and_bad_blocks() {
let mut g = unit(ElementKind::Hex8, [4, 1, 1]);
+5 -2
View File
@@ -98,7 +98,8 @@ fn key(nodes: impl Iterator<Item = u32>) -> Vec<u32> {
k
}
/// The faces (by sorted corners, with their sorted nodes) and the corner edges of elements.
/// The sides (faces of solids, edges of 2D elements; by sorted corners, with their sorted
/// nodes) and the corner edges of elements.
struct Skin {
faces: BTreeMap<Vec<u32>, Vec<u32>>,
edges: BTreeSet<[u32; 2]>,
@@ -112,7 +113,9 @@ impl Skin {
let mut all_edges = BTreeSet::new();
for (kind, nodes) in elements {
let corners = kind.corner_count();
for local in kind.faces() {
// The sides that join neighbours: faces of solids, edges of 2D elements.
let sides = if kind.dimension() == 3 { kind.faces() } else { kind.edges() };
for local in sides {
let cs: Vec<u32> = local.iter().filter(|&&i| i < corners).filter_map(|&i| nodes.get(i).copied()).collect();
let entry = count.entry(key(cs.iter().copied())).or_insert_with(|| (0, key(local.iter().filter_map(|&i| nodes.get(i).copied())), cs));
entry.0 += 1;