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
@@ -47,6 +47,9 @@ impl From<KernelError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// SolidCraft's linear modelling tolerance (mm): points closer than this are the same point.
|
||||
const LINEAR_TOLERANCE: f64 = solidcraft_geom::LINEAR_TOL;
|
||||
|
||||
/// A solid body.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Shape {
|
||||
@@ -86,6 +89,11 @@ pub struct SurfaceMesh {
|
||||
pub triangles: Vec<[u32; 3]>,
|
||||
/// The B-rep face each triangle lies on.
|
||||
pub tri_face: Vec<u32>,
|
||||
/// Copies of one point closer than this are the same point (0: only identical coordinates).
|
||||
/// A [`Shape::tessellate`] result is a triangle soup whose faces each carry their own copy of
|
||||
/// the boundary points they share, accurate to the kernel's linear tolerance, so two copies
|
||||
/// can differ by up to twice it; boundary surface preparation welds at this distance.
|
||||
pub weld_distance: f64,
|
||||
}
|
||||
|
||||
fn xyz(v: solidcraft_geom::Vec3) -> [f64; 3] {
|
||||
@@ -129,7 +137,12 @@ impl Shape {
|
||||
return Err(Error::Invalid(format!("tessellation tolerance must be a positive number, got {tolerance}")));
|
||||
}
|
||||
let m = guard("tessellate", || self.body.tessellate(tolerance))?;
|
||||
Ok(SurfaceMesh { positions: m.positions.into_iter().map(xyz).collect(), triangles: m.triangles, tri_face: m.tri_face })
|
||||
Ok(SurfaceMesh {
|
||||
positions: m.positions.into_iter().map(xyz).collect(),
|
||||
triangles: m.triangles,
|
||||
tri_face: m.tri_face,
|
||||
weld_distance: 2.0 * LINEAR_TOLERANCE,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ fn surface(mut o: Object, k: usize, id: u32, xf: &Xf, unit: f64) -> Result<Tagge
|
||||
}
|
||||
let faces = o.faces.unwrap_or_else(|| o.tags.iter().max().map_or(1, |f| f.saturating_add(1)));
|
||||
let name = if o.name.is_empty() { format!("Mesh{}", k + 1) } else { o.name };
|
||||
Ok(TaggedSurface { name, mesh: SurfaceMesh { positions: o.positions, triangles: o.triangles, tri_face: o.tags }, faces })
|
||||
Ok(TaggedSurface { name, mesh: SurfaceMesh { positions: o.positions, triangles: o.triangles, tri_face: o.tags, weld_distance: 0.0 }, faces })
|
||||
}
|
||||
|
||||
/// [`read_3mf`] of a file: a regular file of at most [`MAX_FILE_BYTES`] (SolidCraft's virtual
|
||||
@@ -482,6 +482,8 @@ mod tests {
|
||||
assert_eq!(bytes, write_tagged(&[("Block", &mesh, faces), ("Again", &mesh, faces)]), "deterministic bytes");
|
||||
let got = read_3mf(&bytes).unwrap();
|
||||
assert_eq!(got.len(), 2);
|
||||
// The file carries no weld distance.
|
||||
let mesh = SurfaceMesh { weld_distance: 0.0, ..mesh };
|
||||
assert_eq!(got[0], TaggedSurface { name: "Block".into(), mesh: mesh.clone(), faces: 6 });
|
||||
assert_eq!(got[1].name, "Again");
|
||||
assert_eq!(got[1].mesh, mesh);
|
||||
@@ -577,7 +579,11 @@ mod tests {
|
||||
}
|
||||
assert!(read_3mf(b"").is_err());
|
||||
assert!(read_3mf(b"PK\x03\x04garbage").is_err());
|
||||
let good = write_tagged(&[("T", &SurfaceMesh { positions: vec![[0.0; 3]; 3], triangles: vec![[0, 1, 2]], tri_face: vec![0] }, 1)]);
|
||||
let good = write_tagged(&[(
|
||||
"T",
|
||||
&SurfaceMesh { positions: vec![[0.0; 3]; 3], triangles: vec![[0, 1, 2]], tri_face: vec![0], weld_distance: 0.0 },
|
||||
1,
|
||||
)]);
|
||||
for cut in [10, good.len() / 2, good.len() - 1] {
|
||||
assert!(read_3mf(&good[..cut]).is_err(), "truncated at {cut}");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user