forked from 1b-agent/fieldcraft-upstream
Boundary surface preparation (weld, manifold/orientation, exact self-intersection, feature edges) and mesh.surface.check (#15)
* mesh: boundary surface preparation (weld, manifold and orientation checks, exact self-intersection, feature edges) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * engine: mesh.surface.check on an open design's body or an imported surface Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * engine: struct-init the test session Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * surface: weld at an absolute distance too; the engine welds CAD tessellations at twice SolidCraft's linear tolerance Faces of a SolidCraft tessellation carry their own copies of shared boundary points, which agree only to within the kernel's tolerance (up to ~2e-6 mm on the test corpus). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: surface preparation in the literature survey, architecture and roadmap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * surface: simplify after review - The weld distance travels with the mesh (SurfaceMesh::weld_distance, set by Shape::tessellate), so every consumer welds a tessellation correctly without opting in. - Exact tests are infallible on predicates::orient2d_projected (shared with generic_orient2d), and compute each vertex's plane side once per pair with an early reject. - One Box3 for the weld, the BVH and shells; a Half struct for edges; the weld's two lookups share one loop, with chained grid cells; cached normals for feature edges; shared parameter helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * corpus: boundary surface preparation as a corpus stage 20 of 21 parts are valid boundaries with their expected genus; cross-cylinders' tessellation has cracks and flipped triangles where its two intersection curves meet. 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:
24 files changed
+1987
-60
No files matched your search
@@ -14,6 +14,7 @@ use std::time::Instant;
|
||||
|
||||
use crate::corpus::{self, CATEGORIES, Part};
|
||||
use modelcraft_geometry::{Design, Measure, SurfaceMesh};
|
||||
use modelcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||
|
||||
pub const FILE: &str = "docs/meshing/corpus.md";
|
||||
/// Chordal tolerance of the boundary surface, relative to the part's bounding-box diagonal.
|
||||
@@ -40,7 +41,7 @@ pub trait Stage {
|
||||
|
||||
/// The stages, in column order. Meshers add theirs here.
|
||||
fn stages() -> Vec<Box<dyn Stage>> {
|
||||
vec![Box::new(SurfaceVolume)]
|
||||
vec![Box::new(SurfaceVolume), Box::new(BoundaryCheck)]
|
||||
}
|
||||
|
||||
/// The boundary surface's enclosed volume against SolidCraft's measured volume and the closed
|
||||
@@ -60,6 +61,31 @@ impl Stage for SurfaceVolume {
|
||||
}
|
||||
}
|
||||
|
||||
/// Boundary surface preparation (`modelcraft_mesh::surface`): welded, closed, manifold, outward
|
||||
/// and free of self-intersections, or the first defects found, which keep the part from being
|
||||
/// volume meshed.
|
||||
struct BoundaryCheck;
|
||||
|
||||
impl Stage for BoundaryCheck {
|
||||
fn headers(&self) -> &'static [&'static str] {
|
||||
&["boundary", "genus", "feature edges", "check ms"]
|
||||
}
|
||||
|
||||
fn run(&self, l: &Loaded) -> Result<Vec<String>, String> {
|
||||
let start = Instant::now();
|
||||
let (mut vertices, mut genus, mut features) = (0, Vec::new(), 0);
|
||||
for (body, mesh) in l.design.bodies.iter().zip(&l.surfaces) {
|
||||
let options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() };
|
||||
let s = BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options).map_err(|e| format!("{}: {e}", body.name))?;
|
||||
vertices += s.points().len();
|
||||
genus.extend(s.shells().iter().map(|sh| if sh.void { format!("{} (void)", sh.genus) } else { sh.genus.to_string() }));
|
||||
features += s.feature_edges().len();
|
||||
}
|
||||
let ms = start.elapsed().as_secs_f64() * 1e3;
|
||||
Ok(vec![format!("ok, {vertices} vertices"), genus.join(", "), features.to_string(), format!("{ms:.1}")])
|
||||
}
|
||||
}
|
||||
|
||||
fn load(part: &Part) -> Result<(Loaded<'_>, f64), String> {
|
||||
let start = Instant::now();
|
||||
let design = corpus::load(part).map_err(|e| e.to_string())?;
|
||||
@@ -180,3 +206,22 @@ fn document(table: &str, parts: &[Part]) -> String {
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The analytic shapes come out of boundary preparation valid, with their genus.
|
||||
#[test]
|
||||
fn analytic_parts_are_valid_boundaries() {
|
||||
let expected =
|
||||
[("analytic-sphere", "0"), ("analytic-torus", "1"), ("analytic-cylinder", "0"), ("analytic-pipe", "1"), ("analytic-plate-hole", "1")];
|
||||
let parts = corpus::analytic_parts();
|
||||
for (name, genus) in expected {
|
||||
let part = parts.iter().find(|p| p.name == name).unwrap();
|
||||
let (loaded, _) = load(part).unwrap();
|
||||
let cells = BoundaryCheck.run(&loaded).unwrap();
|
||||
assert_eq!(cells[1], genus, "{name}: {cells:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user