forked from 1b-agent/fieldcraft-upstream
Structured hex: extrude, revolve, sweep and mapped all-hex meshing (M5) (#40)
* Structured hex meshing: multi-level extrude and revolve, one-to-one sweep, mapped blocks (M5) modelcraft_mesh::hex::hex_mesh meshes a face-tagged boundary surface with hexahedra by the first structured method that fits: a straight or rotational multi-level sweep (imprinted cross-section arrangement, cuts at reflex corners, mapped/annulus/free regions), a one-to-one sweep through rings of linking faces (least-squares affine layers with residual correction), or a mapped logical cube (trivariate transfinite interpolation). Interval assignment couples shared curves; boundary quads carry B-rep face tags; the certified validity gate, a tiling check and a fidelity check guard the output; hex20 mid-edge nodes go onto the faces. Shape::face_surfaces exposes the analytic face surfaces; the corpus runner gets a structured hex column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * mesh.hex.generate: structured hex meshing as a command Meshes one body, every body of the open design or an imported surface, replaces the mesh, tags element faces with the B-rep faces (so sets by geometry resolve) and reports per body the method, counts and quality. The quality pass certifies each element once and feeds the validity gate (quality::validity_gate_checks). Shape::size gives the cached body size for defaults instead of a full measure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Hex: disk O-grids, convex interval cost, early size guard, hex20 corpus column Smooth single-loop sections (cylinders, tori, vases) get a butterfly O-grid instead of the free mesher; interval assignment raises counts by the squared relative error so equal sides share increments evenly; a mesh far beyond the element limit is refused before any work and mapped limits are no longer swallowed. The corpus reports hex8 and hex20 quality. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Catalogue: structured hex commands ready (mesh.hex.sweep, mesh.hex.mapped, mesh.hex.generate) mesh.hex.sweep and mesh.hex.mapped run mesh.hex.generate with the sweeping methods or the mapped one; mesh.hex.generate is the sweep entry's related command. The planned M6 octree entry moves to mesh.hex.octree (its icon renamed with it). docs/features.md regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Docs: structured hex literature (B8), architecture, roadmap, parity; corpus hex column Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Hex: simplify review fixes Prepare each sweep frame's cross-section once (arrangement, cuts, fill) and reuse it across refinement retries; candidate frames come from one setup pass. Edge triangles come from the feature edges, layer counts go through size::edge_divisions, sweep residual weights are computed once per node, flux shape functions live on the stack, and the arrangement's exact side test and distances serve the region probe. The engine takes the boundary options mesh.tet.generate takes and names unnamed faces by index like it; the corpus hex20 run reuses the winning method; the mapped entry lists mesh.hex.generate too; the octree icon is redrawn under its new id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Regenerate docs/features.md and docs/meshing/corpus.md after rebasing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Regenerate docs/features.md and docs/meshing/corpus.md after rebasing onto main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Regenerate docs/features.md and docs/meshing/corpus.md after rebasing onto main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Regenerate docs/features.md and docs/meshing/corpus.md after rebasing onto main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Regenerate docs/features.md after rebasing onto main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Regenerate docs/features.md after rebasing onto main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Regenerate docs/features.md after rebasing onto main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 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:
32 files changed
+5006
-47
No files matched your search
@@ -13,8 +13,10 @@ use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::corpus::{self, CATEGORIES, Part};
|
||||
use modelcraft_geometry::{Design, Measure, SurfaceMesh};
|
||||
use modelcraft_geometry::{Design, FaceSurface, Measure, SurfaceMesh};
|
||||
use modelcraft_mesh::cdt::{CdtOptions, tetrahedralize};
|
||||
use modelcraft_mesh::hex::{HexOptions, Surface, hex_mesh};
|
||||
use modelcraft_mesh::size::UniformSize;
|
||||
use modelcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||
|
||||
pub const FILE: &str = "docs/meshing/corpus.md";
|
||||
@@ -42,7 +44,7 @@ pub trait Stage {
|
||||
|
||||
/// The stages, in column order. Meshers add theirs here.
|
||||
fn stages() -> Vec<Box<dyn Stage>> {
|
||||
vec![Box::new(SurfaceVolume), Box::new(BoundaryCheck), Box::new(TetCdt)]
|
||||
vec![Box::new(SurfaceVolume), Box::new(BoundaryCheck), Box::new(TetCdt), Box::new(StructuredHex)]
|
||||
}
|
||||
|
||||
/// The boundary surface's enclosed volume against SolidCraft's measured volume and the closed
|
||||
@@ -114,6 +116,81 @@ impl Stage for TetCdt {
|
||||
}
|
||||
}
|
||||
|
||||
/// Element size of the structured hex stage, relative to the part's bounding-box diagonal.
|
||||
const HEX_RELATIVE_SIZE: f64 = 1.0 / 25.0;
|
||||
/// Characters of a "not meshed" reason kept in the table.
|
||||
const REASON_CHARS: usize = 240;
|
||||
|
||||
/// Structured all-hex meshing (`modelcraft_mesh::hex`): the method that meshed each body, the
|
||||
/// element count and the smallest scaled Jacobian of the hex8 mesh and of the hex20 mesh (mid-edge
|
||||
/// nodes on the geometry), or why no structured method fits.
|
||||
struct StructuredHex;
|
||||
|
||||
impl Stage for StructuredHex {
|
||||
fn headers(&self) -> &'static [&'static str] {
|
||||
&["hex", "hex elements", "hex8 min SJ", "hex20 min SJ", "hex ms"]
|
||||
}
|
||||
|
||||
fn run(&self, l: &Loaded) -> Result<Vec<String>, String> {
|
||||
let start = Instant::now();
|
||||
let (lo, hi) = l.measures.iter().fold(([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]), |(mut lo, mut hi), m| {
|
||||
for i in 0..3 {
|
||||
lo[i] = lo[i].min(m.bbox_min[i]);
|
||||
hi[i] = hi[i].max(m.bbox_max[i]);
|
||||
}
|
||||
(lo, hi)
|
||||
});
|
||||
let diagonal = (0..3).map(|i| (hi[i] - lo[i]).powi(2)).sum::<f64>().sqrt();
|
||||
let size = UniformSize(HEX_RELATIVE_SIZE * diagonal);
|
||||
let linear = HexOptions { chordal_tolerance: l.tolerance, ..HexOptions::default() };
|
||||
let quadratic = HexOptions { quadratic: true, ..linear.clone() };
|
||||
let (mut methods, mut elements, mut sj8, mut sj20) = (Vec::new(), 0usize, f64::INFINITY, Ok(f64::INFINITY));
|
||||
for (body, mesh) in l.design.bodies.iter().zip(&l.surfaces) {
|
||||
let surface_options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() };
|
||||
let s = BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &surface_options)
|
||||
.map_err(|e| format!("{}: {e}", body.name))?;
|
||||
let faces: Vec<Option<Surface>> =
|
||||
body.face_surfaces().map_err(|e| format!("{}: {e}", body.name))?.into_iter().map(|f| f.map(analytic)).collect();
|
||||
let m_method = match hex_mesh(&s, &faces, &size, &linear) {
|
||||
Ok(m) => {
|
||||
methods.push(m.report.method.name());
|
||||
elements += m.element_count();
|
||||
sj8 = sj8.min(m.report.min_scaled_jacobian);
|
||||
m.report.method
|
||||
}
|
||||
Err(e) => {
|
||||
let reason: String = e.to_string().chars().take(REASON_CHARS).collect();
|
||||
let ms = start.elapsed().as_secs_f64() * 1e3;
|
||||
return Ok(vec![format!("no: {}: {reason}", body.name), String::new(), String::new(), String::new(), format!("{ms:.0}")]);
|
||||
}
|
||||
};
|
||||
// The hex20 run goes straight to the method that meshed the body.
|
||||
let quadratic = HexOptions { method: m_method, ..quadratic.clone() };
|
||||
sj20 = match (sj20, hex_mesh(&s, &faces, &size, &quadratic)) {
|
||||
(Ok(a), Ok(m)) => Ok(a.min(m.report.min_scaled_jacobian)),
|
||||
(Err(e), _) => Err(e),
|
||||
(_, Err(e)) => Err(format!("{}: {}", body.name, e.to_string().chars().take(REASON_CHARS).collect::<String>())),
|
||||
};
|
||||
}
|
||||
methods.sort_unstable();
|
||||
methods.dedup();
|
||||
let ms = start.elapsed().as_secs_f64() * 1e3;
|
||||
let sj20 = sj20.map_or_else(|e| format!("error: {e}"), |v| format!("{v:.3}"));
|
||||
Ok(vec![methods.join(", "), elements.to_string(), format!("{sj8:.3}"), sj20, format!("{ms:.0}")])
|
||||
}
|
||||
}
|
||||
|
||||
/// The geometry's analytic face surface in the mesher's terms.
|
||||
pub fn analytic(s: FaceSurface) -> Surface {
|
||||
match s {
|
||||
FaceSurface::Plane { normal, point } => Surface::Plane { point, normal },
|
||||
FaceSurface::Cylinder { origin, axis, radius } => Surface::Cylinder { origin, axis, radius },
|
||||
FaceSurface::Cone { apex, axis, half_angle } => Surface::Cone { apex, axis, half_angle },
|
||||
FaceSurface::Sphere { center, radius } => Surface::Sphere { center, radius },
|
||||
FaceSurface::Torus { center, axis, major, minor } => Surface::Torus { center, axis, major, minor },
|
||||
}
|
||||
}
|
||||
|
||||
fn load(part: &Part) -> Result<(Loaded<'_>, f64), String> {
|
||||
let start = Instant::now();
|
||||
let design = corpus::load(part).map_err(|e| e.to_string())?;
|
||||
|
||||
Reference in new issue
Block a user