mesh: the tet mesher takes its input in a canonical order (#115)

SolidCraft lists a solid's faces in a different order from run to run
(the rounded box, for one), which reordered the triangle soup and the
face tags with every coordinate the same; welding keeps the first copy
it meets and every later stage visits ids in order, so the tet mesh
changed between runs. CanonicalSoup sorts the triangles by their
coordinates, numbers vertices by first use and face tags by first
appearance, and maps the tags back; mesh.tet.generate and the corpus
use it.

Tests: the washer's tet10 mesh has a fixed fingerprint (identical on
arm64 and x86-64 builds); a reordered, renumbered input gives the same
mesh; heavy: rounded-box and filleted-bracket mesh bit for bit the same
on repeated runs and with 1 and 8 threads.

The bracket oracle cases stay unfrozen: SolidCraft's tessellation
itself differs in the last bits between arm64 and x86-64. The coarse
case moves to 203.6 MPa with the canonical order; its band is 5%.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 17:43:30 -04:00
1 parent 20281e5959
commit 9c1c8f75c2
10 files changed
+341 -15

No files matched your search

+9 -4
View File
@@ -7,6 +7,7 @@ use fieldcraft_mesh::cdt::{CdtOptions, tetrahedralize};
use fieldcraft_mesh::delaunay::Triangulation;
use fieldcraft_mesh::quality::{self, QualityReport};
use fieldcraft_mesh::surface::{BoundarySurface, Defect, FeatureKind, SurfaceOptions};
use fieldcraft_mesh::tet::canonical::CanonicalSoup;
use fieldcraft_mesh::tet::geometry::AnalyticSurface;
use fieldcraft_mesh::tet::improve::ImproveOptions;
use fieldcraft_mesh::tet::{Order, QualityMesh, QualityOptions, TetQuality, quality_mesh, raw_mesh};
@@ -288,15 +289,19 @@ pub(super) fn mesh_body(
Some(_) => Vec::new(),
};
fieldcraft_model::progress::stage(&format!("{}: boundary", input.name), None, "");
let surface = BoundarySurface::new(&input.mesh.positions, &input.mesh.triangles, &input.mesh.tri_face, &surface_options(p, &input.mesh)?)?;
// The mesh depends on the geometry only, not on the order the kernel lists it in.
let soup = CanonicalSoup::new(&input.mesh.positions, &input.mesh.triangles, &input.mesh.tri_face)?;
let surface = BoundarySurface::new(&soup.positions, &soup.triangles, &soup.tri_face, &surface_options(p, &input.mesh)?)?;
fieldcraft_model::progress::stage(&format!("{}: Delaunay", input.name), None, "");
let tets = tetrahedralize(&surface, cdt)?;
let mesh = if quality {
let options = QualityOptions { seeds: input.seeds.clone(), ..options.clone() };
quality_mesh(&surface, &tets, analytic, &options)?
let mut mesh = if quality {
let frozen_faces = options.frozen_faces.iter().filter_map(|&f| soup.canonical_tag(f)).collect();
let options = QualityOptions { seeds: input.seeds.clone(), frozen_faces, ..options.clone() };
quality_mesh(&surface, &tets, soup.analytic(&analytic), &options)?
} else {
raw_mesh(&surface, &tets)?
};
soup.restore_tags(&mut mesh);
Ok(BodyTets {
name: input.name.clone(),
face_names: input.face_names.clone(),