forked from 1b-agent/fieldcraft-upstream
2D elements: plane stress, plane strain and axisymmetric sections on the DOF framework; mesh.planar (M4) (#39)
Model: planar section kinds with Extent, element edges in sets, edge-weighted 2D loads,
centrifugal load, 2D model checks. fem: Plane kernels on Basis::{Tri3,Tri6,Quad4,Quad8}
declaring ElementDofs::new(DofSet::PLANE); QM6 incompatible modes with the
Wilson-Ibrahimbegovic correction. Solver: Kernel enum on the DofLayout; 3D results
bit-identical (hash-checked). Decks: Abaqus CPS/CPE/CAX, Nastran membranes and RFORCE,
LS-DYNA ELFORM 12/13. Mesh: free triangle mesher, planar region meshing, face boundaries.
Engine: mesh.planar with tagged edges. Analytic verification suite in solver/tests/plane.rs.
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:
42 files changed
+3270
-128
No files matched your search
@@ -160,6 +160,51 @@ pub fn free_quad_mesh(loops: &[Vec<[f64; 2]>], size: &dyn SizeField, opts: &Quad
|
||||
free::mesh(loops, size, opts)
|
||||
}
|
||||
|
||||
/// A planar triangle mesh.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TriMesh {
|
||||
/// Node coordinates; `nodes[..boundary_nodes]` are the input boundary points in input order.
|
||||
pub nodes: Vec<[f64; 2]>,
|
||||
/// Counterclockwise corner node ids (the `Tri3` node order).
|
||||
pub triangles: Vec<[u32; 3]>,
|
||||
pub boundary_nodes: usize,
|
||||
}
|
||||
|
||||
/// Free triangle mesh of a planar region on prescribed boundary nodes: the constrained Delaunay
|
||||
/// triangulation of the loops ([`cdt`]: Bowyer–Watson, Sloan 1993 recovery), refined to the size
|
||||
/// field by Ruppert-style circumcentre insertion that never splits a boundary edge (Steiner points
|
||||
/// keep a clearance of half the local boundary spacing from the boundary), so the boundary nodes
|
||||
/// are exactly the input points. Every triangle is checked counterclockwise with exact
|
||||
/// predicates. Loops as for [`free_quad_mesh`], with any number of points (at least 3 each).
|
||||
pub fn free_tri_mesh(loops: &[Vec<[f64; 2]>], size: &dyn SizeField, opts: &QuadOptions) -> Result<TriMesh> {
|
||||
let region = domain::region(loops)?;
|
||||
let mut chords = Vec::new();
|
||||
for l in ®ion.loops {
|
||||
for (i, &a) in l.iter().enumerate() {
|
||||
let b = l.get((i + 1) % l.len()).copied().ok_or_else(|| Error::Failed("loop index".into()))?;
|
||||
let (pa, pb) = (*region.point(a)?, *region.point(b)?);
|
||||
let (ca, cb) = (pa.coords(), pb.coords());
|
||||
let m = Point2::new(snap_to_domain(0.5 * (ca[0] + cb[0])), snap_to_domain(0.5 * (ca[1] + cb[1])))?;
|
||||
chords.push((pa, pb, m));
|
||||
}
|
||||
}
|
||||
let guard = guard::Guard::new(&chords)?;
|
||||
let mut cdt = cdt::Cdt::build(®ion, opts.max_elements.saturating_add(4 * region.pts.len() + 16))?;
|
||||
cdt.refine(size, 1.0, &|p, h| guard.allows(p, cdt::CLEARANCE * h))?;
|
||||
let tri = cdt.into_triangulation();
|
||||
if tri.tris.len() > opts.max_elements {
|
||||
return Err(Error::Limit(format!("the mesh would have {} triangles; the limit is {}", tri.tris.len(), opts.max_elements)));
|
||||
}
|
||||
for t in &tri.tris {
|
||||
let [a, b, c] = t.map(|v| tri.p2.get(v as usize));
|
||||
match (a, b, c) {
|
||||
(Some(a), Some(b), Some(c)) if crate::predicates::orient2d(a, b, c) == Sign::Positive => {}
|
||||
_ => return Err(Error::Failed("a triangle is inverted or degenerate".into())),
|
||||
}
|
||||
}
|
||||
Ok(TriMesh { nodes: tri.pts, triangles: tri.tris, boundary_nodes: region.pts.len() })
|
||||
}
|
||||
|
||||
/// Mapped mesh of a four-sided region: `boundary` lists the region's boundary nodes once (either
|
||||
/// orientation), and `corners` are the positions in it of the four corners in boundary order.
|
||||
/// Opposite sides need the same number of divisions. The grid is transfinite interpolation
|
||||
|
||||
@@ -537,3 +537,34 @@ fn unsmoothed_meshes_are_valid() {
|
||||
let m = free_quad_mesh(&loops, &UniformSize(0.2), &QuadOptions { smoothing_sweeps: 0, ..opts() }).unwrap();
|
||||
check(&m, &loops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_triangle_mesh_of_a_plate_with_a_hole() {
|
||||
// Odd point counts are fine for triangles.
|
||||
let outer = rect(0.0, 0.0, 10.0, 6.0, 9, 5);
|
||||
let hole: Vec<[f64; 2]> = circle(4.0, 3.0, 1.5, 17).into_iter().rev().collect();
|
||||
let loops = vec![outer.clone(), hole.clone()];
|
||||
let m = free_tri_mesh(&loops, &UniformSize(0.7), &opts()).unwrap();
|
||||
assert_eq!(m.boundary_nodes, outer.len() + hole.len());
|
||||
let boundary: Vec<[f64; 2]> = outer.iter().chain(&hole).copied().collect();
|
||||
assert_eq!(&m.nodes[..m.boundary_nodes], boundary.as_slice(), "boundary nodes are the input points");
|
||||
let area: f64 = m
|
||||
.triangles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let [a, b, c] = t.map(|v| m.nodes[v as usize]);
|
||||
let s = 0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]));
|
||||
assert!(s > 0.0, "counterclockwise");
|
||||
s
|
||||
})
|
||||
.sum();
|
||||
let want = signed_area(&outer).abs() - signed_area(&hole).abs();
|
||||
assert!((area - want).abs() < 1e-9 * want, "{area} vs {want}");
|
||||
assert!(m.triangles.len() > 100, "refined to the size: {}", m.triangles.len());
|
||||
// Deterministic.
|
||||
assert_eq!(free_tri_mesh(&loops, &UniformSize(0.7), &opts()).unwrap(), m);
|
||||
// Hostile.
|
||||
assert!(free_tri_mesh(&[vec![[0.0, 0.0], [1.0, 0.0]]], &UniformSize(0.5), &opts()).is_err());
|
||||
assert!(free_tri_mesh(std::slice::from_ref(&outer), &UniformSize(0.001), &QuadOptions { max_elements: 100, ..opts() }).is_err());
|
||||
assert!(free_tri_mesh(&[vec![[0.0, 0.0], [f64::NAN, 0.0], [0.0, 1.0]]], &UniformSize(0.5), &opts()).is_err());
|
||||
}
|
||||
Reference in new issue
Block a user