mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
kernel-cases: watertight checks the display mesh, welding vertices by distance
Two faults made simple curved solids look open (#818's blind hole: 80 open edges, a stadium slot: 52): - It meshed with `refine`, the measuring mesh, which splits triangles across curved faces on their own and leaves T-junctions on the edges they share with neighbours. It now checks the display mesh, whose faces share each edge's points. - It matched vertices by rounding to a 1e-6 mm grid, which splits points a rounding error apart across a cell boundary. It now joins vertices closer than `tolerance.weld_mm` (default 1e-4 mm). Both cases now have 0 open edges, also with the weld at 1e-6 mm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,18 +59,55 @@ fn scratch(area: &str, name: &str) -> Result<String, String> {
|
||||
/// Chord tolerance of the mesh a `watertight` case is checked on (mm).
|
||||
const MESH_TOL: f64 = 0.01;
|
||||
|
||||
/// A mesh's edges used by one triangle only (open) and by more than two (over-shared), with
|
||||
/// vertices matched by position (a mesh may repeat a vertex for each face).
|
||||
fn open_edges(positions: &[solidcraft_engine::geom::Vec3], triangles: &[[u32; 3]]) -> (usize, usize) {
|
||||
let key = |i: u32| positions.get(i as usize).map(|p| [p.x, p.y, p.z].map(|x| (x * 1e6).round() as i64));
|
||||
let mut uses: std::collections::HashMap<([i64; 3], [i64; 3]), usize> = std::collections::HashMap::new();
|
||||
/// A mesh's edges used by one triangle only (open) and by more than two (over-shared). Vertices
|
||||
/// closer than `weld` (mm) are one vertex: a mesh repeats a vertex for each face, and faces
|
||||
/// meeting at an edge may place it a rounding error apart.
|
||||
fn open_edges(positions: &[solidcraft_engine::geom::Vec3], triangles: &[[u32; 3]], weld: f64) -> (usize, usize) {
|
||||
use std::collections::HashMap;
|
||||
let weld = if weld.is_finite() && weld > 0.0 { weld } else { 1e-4 };
|
||||
let cell = |p: &solidcraft_engine::geom::Vec3| [p.x, p.y, p.z].map(|x| (x / weld).floor() as i64);
|
||||
// Union-find over positions, joining each vertex with any within `weld` in the 27 cells round it.
|
||||
let mut parent: Vec<usize> = (0..positions.len()).collect();
|
||||
fn find(parent: &mut [usize], mut i: usize) -> usize {
|
||||
while let Some(&p) = parent.get(i) {
|
||||
if p == i {
|
||||
break;
|
||||
}
|
||||
let gp = parent.get(p).copied().unwrap_or(p);
|
||||
if let Some(slot) = parent.get_mut(i) {
|
||||
*slot = gp;
|
||||
}
|
||||
i = p;
|
||||
}
|
||||
i
|
||||
}
|
||||
let mut grid: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
|
||||
for (i, p) in positions.iter().enumerate() {
|
||||
let c = cell(p);
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
let Some(near) = grid.get(&[c[0] + dx, c[1] + dy, c[2] + dz]) else { continue };
|
||||
for &j in near {
|
||||
if positions.get(j).is_some_and(|q| q.dist(*p) <= weld) {
|
||||
let (a, b) = (find(&mut parent, i), find(&mut parent, j));
|
||||
if let Some(slot) = parent.get_mut(a) {
|
||||
*slot = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
grid.entry(c).or_default().push(i);
|
||||
}
|
||||
let mut uses: HashMap<(usize, usize), usize> = HashMap::new();
|
||||
for t in triangles {
|
||||
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
|
||||
let (Some(a), Some(b)) = (key(a), key(b)) else { continue };
|
||||
if a == b {
|
||||
continue;
|
||||
let (a, b) = (find(&mut parent, a as usize), find(&mut parent, b as usize));
|
||||
if a != b {
|
||||
*uses.entry((a.min(b), a.max(b))).or_default() += 1;
|
||||
}
|
||||
*uses.entry(if a < b { (a, b) } else { (b, a) }).or_default() += 1;
|
||||
}
|
||||
}
|
||||
(uses.values().filter(|&&n| n == 1).count(), uses.values().filter(|&&n| n > 2).count())
|
||||
@@ -192,8 +229,8 @@ fn check_case(dir: &str) -> Value {
|
||||
shells += m.shells as u64;
|
||||
if want["watertight"].as_bool() == Some(true) {
|
||||
let tol = want["tolerance"]["mesh_mm"].as_f64().unwrap_or(MESH_TOL);
|
||||
let (open, over) = match b.body.tessellate_with(tol, true) {
|
||||
Ok(mesh) => open_edges(&mesh.positions, &mesh.triangles),
|
||||
let (open, over) = match b.body.tessellate(tol) {
|
||||
Ok(mesh) => open_edges(&mesh.positions, &mesh.triangles, want["tolerance"]["weld_mm"].as_f64().unwrap_or(1e-4)),
|
||||
Err(e) => {
|
||||
c.push("watertight", json!(format!("{}: {e}", b.name)), json!("no open edges"), false);
|
||||
continue;
|
||||
|
||||
@@ -87,9 +87,9 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
|
||||
| `body_count`, `faces`, `edges`, `vertices` | Exact counts, where the geometry fixes them. Faces and edges count whole analytic surfaces and curves once (a full cylinder side is one face). |
|
||||
| `volume_mm3`, `area_mm2`, `center_of_mass`, `bbox` | As in `tests/oracle`: totals over every body, relative tolerance 1e-3 for volume and area, 1e-3 of the box diagonal for positions. |
|
||||
| `faces`, `edges`, `vertices`, `shells`, `body_count` as `[min, max]` | An inclusive range, where the geometry only bounds a count (a modelled thread's faces). |
|
||||
| `watertight` | `true`: each body's mesh (chord tolerance `tolerance.mesh_mm`, default 0.01 mm) has no open and no over-shared edges. |
|
||||
| `watertight` | `true`: each body's display mesh (chord tolerance `tolerance.mesh_mm`, default 0.01 mm) has no open and no over-shared edges, with vertices closer than `tolerance.weld_mm` (default 1e-4 mm) taken as one. |
|
||||
| `timeout_s` | A longer timeout for this case than the run's (at most 1800 s), for cases that are slow by nature. A case that only needs it on a loaded machine shouldn't set it. |
|
||||
| `tolerance` | Optional overrides: `rel`, `abs_mm`, `mesh_mm`. |
|
||||
| `tolerance` | Optional overrides: `rel`, `abs_mm`, `mesh_mm`, `weld_mm`. |
|
||||
| `missing` | A capability SolidCraft has no command or parameter for yet (`"solid.thread has no start count"`): the case is reported **not supported yet** without running, so the gap stays visible. |
|
||||
|
||||
## The complexity ladder
|
||||
|
||||
Reference in New Issue
Block a user