forked from 1b-agent/fieldcraft-upstream
engine: mesh.hex.octree command
Replaces the mesh with the octree all-hex mesh of a body, an imported surface or every body of the open design, and tags boundary element faces with their B-rep faces so sets by geometry resolve. Size, tolerance, refinement depth, element cap, attempts and optimiser sweeps are validated and capped; the element cap defaults to the session limit (`Limits::max_hex_elements`), which the hostile-params test keeps small. Body input and face tagging are shared with mesh.hex.generate. The catalogue's Automatic Hex entry is ready. 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
3587b0de74
commit
7d2696057f
8 files changed
+221
-21
No files matched your search
@@ -3,7 +3,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use fieldcraft_geometry::{FaceSurface, Shape, SurfaceMesh};
|
||||
use fieldcraft_mesh::hex::{Cell, HexMesh, HexMethod, HexOptions, MAX_CELLS, Surface, hex_mesh, hex_mesh_cells};
|
||||
use fieldcraft_mesh::hex::{BoundaryFace, Cell, HexMesh, HexMethod, HexOptions, MAX_CELLS, Surface, hex_mesh, hex_mesh_cells};
|
||||
use fieldcraft_mesh::size::UniformSize;
|
||||
use fieldcraft_mesh::surface::BoundarySurface;
|
||||
use fieldcraft_model::{GeometryRef, Model};
|
||||
@@ -28,13 +28,13 @@ const MIN_RELATIVE_TOLERANCE: f64 = 1e-5;
|
||||
const DEFAULT_RELATIVE_SIZE: f64 = 1.0 / 25.0;
|
||||
const DEFAULT_RELATIVE_TOLERANCE: f64 = 1e-3;
|
||||
|
||||
/// One body to mesh.
|
||||
struct Input<'a> {
|
||||
name: String,
|
||||
mesh: Cow<'a, SurfaceMesh>,
|
||||
/// One body to mesh (also the input of `mesh.hex.octree`).
|
||||
pub(super) struct Input<'a> {
|
||||
pub(super) name: String,
|
||||
pub(super) mesh: Cow<'a, SurfaceMesh>,
|
||||
surfaces: Vec<Option<Surface>>,
|
||||
face_names: Vec<String>,
|
||||
diagonal: f64,
|
||||
pub(super) diagonal: f64,
|
||||
}
|
||||
|
||||
fn analytic(s: FaceSurface) -> Surface {
|
||||
@@ -47,7 +47,7 @@ fn analytic(s: FaceSurface) -> Surface {
|
||||
}
|
||||
}
|
||||
|
||||
fn number(p: &Value, key: &str) -> Result<Option<f64>> {
|
||||
pub(super) fn number(p: &Value, key: &str) -> Result<Option<f64>> {
|
||||
match p.get(key) {
|
||||
None => Ok(None),
|
||||
Some(v) => v
|
||||
@@ -73,7 +73,7 @@ fn shape_input(shape: &Shape, tolerance: Option<f64>) -> Result<Input<'static>>
|
||||
Ok(Input { name: shape.name.clone(), mesh: Cow::Owned(shape.tessellate(tol)?), surfaces, face_names, diagonal: diag })
|
||||
}
|
||||
|
||||
fn inputs<'a>(s: &'a Session, p: &Value) -> Result<Vec<Input<'a>>> {
|
||||
pub(super) fn inputs<'a>(s: &'a Session, p: &Value) -> Result<Vec<Input<'a>>> {
|
||||
let tolerance = number(p, "tolerance")?;
|
||||
match (p.get("body"), p.get("surface")) {
|
||||
(Some(b), None) => {
|
||||
@@ -220,7 +220,7 @@ fn hex_blocks(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
model.set_element(label(e)?, m.kind, nodes)?;
|
||||
model.tag_element(label(e)?, body_tags.get(cell).copied().ok_or_else(|| EngineError::Params("element without a cell".into()))?)?;
|
||||
}
|
||||
tag_faces(&mut model, &inputs, |e| cm.element_cell.get(e as usize).map_or(0, |&c| c as usize), m, 1)?;
|
||||
tag_faces(&mut model, &inputs, |e| cm.element_cell.get(e as usize).map_or(0, |&c| c as usize), &m.boundary, 1)?;
|
||||
s.model = model;
|
||||
let blocks: Vec<Value> =
|
||||
inputs.iter().zip(&cm.blocks).map(|(b, d)| json!({"body": b.name, "divisions": d, "elements": d.iter().product::<usize>()})).collect();
|
||||
@@ -318,7 +318,7 @@ fn generate(s: &mut Session, p: &Value, default: HexMethod, allowed: &[HexMethod
|
||||
model.tag_element(element_label, body_tag)?;
|
||||
element_label = element_label.checked_add(1).ok_or_else(|| EngineError::Params("too many elements".into()))?;
|
||||
}
|
||||
tag_faces(&mut model, std::slice::from_ref(b), |_| 0, m, first_element)?;
|
||||
tag_faces(&mut model, std::slice::from_ref(b), |_| 0, &m.boundary, first_element)?;
|
||||
report.push(json!({
|
||||
"body": b.name,
|
||||
"method": m.report.method.name(),
|
||||
@@ -338,9 +338,15 @@ fn generate(s: &mut Session, p: &Value, default: HexMethod, allowed: &[HexMethod
|
||||
|
||||
/// Tag every boundary element face with the B-rep face it lies on, of the body
|
||||
/// (`bodies[cell(element)]`) its element meshes.
|
||||
fn tag_faces(model: &mut Model, bodies: &[Input<'_>], cell: impl Fn(u32) -> usize, m: &HexMesh, first_element: u32) -> Result<()> {
|
||||
pub(super) fn tag_faces(
|
||||
model: &mut Model,
|
||||
bodies: &[Input<'_>],
|
||||
cell: impl Fn(u32) -> usize,
|
||||
boundary: &[BoundaryFace],
|
||||
first_element: u32,
|
||||
) -> Result<()> {
|
||||
let mut tags: std::collections::BTreeMap<(usize, u32), u32> = std::collections::BTreeMap::new();
|
||||
for f in &m.boundary {
|
||||
for f in boundary {
|
||||
let c = cell(f.element);
|
||||
let tag = match tags.get(&(c, f.brep_face)) {
|
||||
Some(&t) => t,
|
||||
|
||||
@@ -93,7 +93,7 @@ const TET_MAX_STEINER: u64 = 10_000_000;
|
||||
const TET_MAX_TETS: u64 = 50_000_000;
|
||||
|
||||
/// An optional whole-number parameter in `range`.
|
||||
fn optional_count(p: &Value, key: &str, default: u64, range: std::ops::RangeInclusive<u64>) -> Result<usize> {
|
||||
pub(super) fn optional_count(p: &Value, key: &str, default: u64, range: std::ops::RangeInclusive<u64>) -> Result<usize> {
|
||||
let v = match p.get(key) {
|
||||
None => default,
|
||||
Some(v) => v
|
||||
|
||||
@@ -9,6 +9,7 @@ mod geometry;
|
||||
mod hex;
|
||||
pub(crate) mod mesh;
|
||||
mod model;
|
||||
pub(crate) mod octree;
|
||||
mod results;
|
||||
pub(crate) mod solve;
|
||||
mod tags;
|
||||
@@ -36,6 +37,7 @@ pub static ALL: &[CommandSpec] = &[
|
||||
mesh::SURFACE_CHECK,
|
||||
mesh::PLANAR,
|
||||
mesh::TET_GENERATE,
|
||||
octree::HEX_OCTREE,
|
||||
block::BLOCK,
|
||||
hex::HEX_GENERATE,
|
||||
hex::HEX_SWEEP,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Unstructured all-hexahedral meshing (`mesh.hex.octree`) of the open design's bodies or of
|
||||
//! imported surfaces.
|
||||
|
||||
use fieldcraft_mesh::octree::{OctreeHexMesh, OctreeOptions, octree_hex_mesh};
|
||||
use fieldcraft_mesh::size::UniformSize;
|
||||
use fieldcraft_mesh::surface::BoundarySurface;
|
||||
use fieldcraft_model::{ElementKind, GeometryRef, MAX_ELEMENTS};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::hex::{Input, inputs, number, tag_faces};
|
||||
use super::mesh::optional_count;
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
|
||||
pub const HEX_OCTREE: CommandSpec = CommandSpec {
|
||||
id: "mesh.hex.octree",
|
||||
label: "Octree Hex Mesh",
|
||||
params: r#"{"body": "Body1" (default: every body of the open design) or "surface": 0 (an imported surface), "size": 2.0 (element size in the interior, mm; default 1/25 of the bounding-box diagonal), "tolerance": 0.05 (chordal tolerance of the tessellation, mm; default 1e-3 of the diagonal), "max_depth": 3 (refinement levels below the size, each a third, 0..=5), "max_elements": 2000000 (1..=10000000; default the session limit), "attempts": 4 (builds, refining where one failed, 1..=8), "max_sweeps": 300 (optimiser sweeps, 0..=10000), and the boundary options of mesh.tet.generate ("weld", "weld_distance", "feature_angle")} — replaces the mesh with an unstructured all-hex8 mesh of any part: an adaptive 3-refinement grid with conforming transition templates, a core inside the part, two boundary layers that capture every B-rep face boundary and sharp edge, and untangling. Every element is proven valid; boundary element faces are tagged with their B-rep faces (named as in geometry.bodies; by index when a face has no name), so sets by geometry resolve. Reports per body the elements, quality, volume and grid statistics"#,
|
||||
kind: CommandKind::Model,
|
||||
run: hex_octree,
|
||||
};
|
||||
|
||||
/// Smallest element size, relative to the bounding-box diagonal (the grid's cell count grows
|
||||
/// with the cube of its inverse).
|
||||
const MIN_RELATIVE_SIZE: f64 = 1e-3;
|
||||
/// Default element size, relative to the bounding-box diagonal.
|
||||
const DEFAULT_RELATIVE_SIZE: f64 = 1.0 / 25.0;
|
||||
/// Caps on the options.
|
||||
const MAX_DEPTH: u64 = 5;
|
||||
const MAX_ATTEMPTS: u64 = 8;
|
||||
const MAX_SWEEPS: u64 = 10_000;
|
||||
const MAX_HEX_ELEMENTS: u64 = 10_000_000;
|
||||
/// The session default of `max_elements` ([`crate::Limits::max_hex_elements`]).
|
||||
pub(crate) const DEFAULT_MAX_ELEMENTS: usize = 2_000_000;
|
||||
|
||||
fn hex_octree(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let size = number(p, "size")?;
|
||||
let defaults = OctreeOptions::default();
|
||||
let options = OctreeOptions {
|
||||
max_depth: u8::try_from(optional_count(p, "max_depth", u64::from(defaults.max_depth), 0..=MAX_DEPTH)?).unwrap_or(defaults.max_depth),
|
||||
max_elements: optional_count(p, "max_elements", s.limits.max_hex_elements as u64, 1..=MAX_HEX_ELEMENTS)?.min(MAX_ELEMENTS),
|
||||
attempts: optional_count(p, "attempts", defaults.attempts as u64, 1..=MAX_ATTEMPTS)?,
|
||||
max_sweeps: optional_count(p, "max_sweeps", defaults.max_sweeps as u64, 0..=MAX_SWEEPS)?,
|
||||
..defaults
|
||||
};
|
||||
let bodies = inputs(s, p)?;
|
||||
let mut meshes: Vec<(Input, OctreeHexMesh)> = Vec::with_capacity(bodies.len());
|
||||
let mut total = 0usize;
|
||||
for b in bodies {
|
||||
if !(b.diagonal > 0.0 && b.diagonal.is_finite()) {
|
||||
return Err(EngineError::Params(format!("body `{}` has no extent", b.name)));
|
||||
}
|
||||
let h = size.unwrap_or(DEFAULT_RELATIVE_SIZE * b.diagonal);
|
||||
if h < MIN_RELATIVE_SIZE * b.diagonal {
|
||||
return Err(EngineError::Params(format!("`size` must be at least {} mm for body `{}`", MIN_RELATIVE_SIZE * b.diagonal, b.name)));
|
||||
}
|
||||
let surface_options = super::mesh::surface_options(p, &b.mesh)?;
|
||||
let surface = BoundarySurface::new(&b.mesh.positions, &b.mesh.triangles, &b.mesh.tri_face, &surface_options)
|
||||
.map_err(|e| EngineError::Params(format!("body `{}`: {e}", b.name)))?;
|
||||
let left = OctreeOptions { max_elements: options.max_elements.saturating_sub(total).max(1), ..options.clone() };
|
||||
let mesh = octree_hex_mesh(&surface, &UniformSize(h), &left).map_err(|e| EngineError::Params(format!("body `{}`: {e}", b.name)))?;
|
||||
total += mesh.hexes.len();
|
||||
meshes.push((b, mesh));
|
||||
}
|
||||
// Replace the mesh: definitions stay, nodes, elements and geometry tags are new.
|
||||
let mut model = s.model.without_mesh();
|
||||
let mut report = Vec::with_capacity(meshes.len());
|
||||
let (mut node_label, mut element_label) = (1u32, 1u32);
|
||||
for (b, m) in &meshes {
|
||||
let first_node = node_label;
|
||||
for xyz in &m.nodes {
|
||||
model.set_node(node_label, *xyz)?;
|
||||
node_label = node_label.checked_add(1).ok_or_else(|| EngineError::Params("too many nodes".into()))?;
|
||||
}
|
||||
let body_tag = model.geometry_tag(&GeometryRef::body(b.name.as_str())?)?;
|
||||
let first_element = element_label;
|
||||
for hx in &m.hexes {
|
||||
model.set_element(element_label, ElementKind::Hex8, hx.iter().map(|&i| first_node + i).collect())?;
|
||||
model.tag_element(element_label, body_tag)?;
|
||||
element_label = element_label.checked_add(1).ok_or_else(|| EngineError::Params("too many elements".into()))?;
|
||||
}
|
||||
tag_faces(&mut model, std::slice::from_ref(b), |_| 0, &m.boundary, first_element)?;
|
||||
let r = &m.report;
|
||||
report.push(json!({
|
||||
"body": b.name,
|
||||
"kind": "hex8",
|
||||
"elements": m.hexes.len(),
|
||||
"nodes": m.nodes.len(),
|
||||
"boundary_faces": m.boundary.len(),
|
||||
"quality": {"min_scaled_jacobian": r.min_scaled_jacobian, "mean_scaled_jacobian": r.mean_scaled_jacobian},
|
||||
"volume": r.volume,
|
||||
"surface_volume": r.surface_volume,
|
||||
"grid": {"cells": r.grid_cells, "core_elements": r.core_elements, "layer_elements": r.layer_elements, "frame": r.frame},
|
||||
"features": {"corners": r.corners, "curves": r.curves, "patches": r.patches},
|
||||
"attempts": r.attempts,
|
||||
"retries": r.retries,
|
||||
"sweeps": r.sweeps,
|
||||
}));
|
||||
}
|
||||
s.model = model;
|
||||
Ok(json!({"bodies": report, "nodes": node_label - 1, "elements": element_label - 1}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use fieldcraft_geometry::open_design;
|
||||
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn opened() -> Session {
|
||||
let plate = Plate::default();
|
||||
Session { design: Some(Arc::new(open_design(&plate_design(&plate).unwrap()).unwrap())), ..Session::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plate_with_a_hole_meshes_all_hex_and_face_sets_resolve() {
|
||||
let mut s = opened();
|
||||
let r = s.execute("mesh.hex.octree", &json!({"body": "Body1", "size": 5.0, "max_depth": 2})).unwrap();
|
||||
let body = &r["bodies"][0];
|
||||
let n = body["elements"].as_u64().unwrap() as usize;
|
||||
assert!(n > 50, "{r}");
|
||||
assert!(body["quality"]["min_scaled_jacobian"].as_f64().unwrap() > 0.0, "{r}");
|
||||
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex8, n)]);
|
||||
let (v, sv) = (body["volume"].as_f64().unwrap(), body["surface_volume"].as_f64().unwrap());
|
||||
assert!((v - sv).abs() < 0.02 * sv, "{r}");
|
||||
// The certified quality report agrees.
|
||||
let q = s.execute("mesh.quality", &json!({})).unwrap();
|
||||
assert_eq!(q["valid"].as_u64().unwrap() as usize, n);
|
||||
// Every boundary face is tagged: the skin of the body set; each named face resolves.
|
||||
s.execute("model.set.create", &json!({"name": "skin", "geometry": [{"body": "Body1"}]})).unwrap();
|
||||
let skin = s.execute("model.set.resolve", &json!({"name": "skin"})).unwrap();
|
||||
assert_eq!(skin["faces"]["count"], body["boundary_faces"], "{skin}");
|
||||
let bodies = s.execute("geometry.bodies", &json!({})).unwrap();
|
||||
for f in bodies[0]["faces"].as_array().unwrap() {
|
||||
let name = f["name"].as_str().unwrap();
|
||||
s.execute("model.set.create", &json!({"name": "f", "geometry": [{"body": "Body1", "face": name}]})).unwrap();
|
||||
let set = s.execute("model.set.resolve", &json!({"name": "f"})).unwrap();
|
||||
assert!(set["faces"]["count"].as_u64().unwrap() > 0, "face {name}: {set}");
|
||||
s.execute("model.set.delete", &json!({"name": "f"})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_params_are_errors_and_change_nothing() {
|
||||
let mut s = opened();
|
||||
let before = s.model.clone();
|
||||
for p in [
|
||||
json!({"body": "Body1", "size": 0}),
|
||||
json!({"body": "Body1", "size": -1}),
|
||||
json!({"body": "Body1", "size": 1e-300}),
|
||||
json!({"body": "Body1", "size": "big"}),
|
||||
json!({"body": "Body1", "tolerance": 1e-300}),
|
||||
json!({"body": "Body1", "tolerance": -1}),
|
||||
json!({"body": "Body1", "max_depth": 9}),
|
||||
json!({"body": "Body1", "max_depth": -1}),
|
||||
json!({"body": "Body1", "max_elements": 0}),
|
||||
json!({"body": "Body1", "max_elements": 1e300}),
|
||||
json!({"body": "Body1", "attempts": 0}),
|
||||
json!({"body": "Body1", "attempts": 100}),
|
||||
json!({"body": "Body1", "max_sweeps": "many"}),
|
||||
json!({"body": "Body1", "size": 5.0, "max_depth": 2, "max_elements": 10}),
|
||||
json!({"body": "Nope"}),
|
||||
json!({"body": 7}),
|
||||
json!({"surface": 0}),
|
||||
json!({"surface": -1}),
|
||||
json!({"body": "Body1", "surface": 0}),
|
||||
] {
|
||||
let r = s.execute("mesh.hex.octree", &p);
|
||||
assert!(r.is_err(), "{p}: {r:?}");
|
||||
assert_eq!(s.model, before, "{p}");
|
||||
}
|
||||
assert!(Session::default().execute("mesh.hex.octree", &json!({})).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user