Exact 3D Delaunay tetrahedralization (Bowyer–Watson, ghost hull, BRIO/Hilbert, verifier) (#14)

* mesh: exact Bowyer-Watson Delaunay tetrahedralization with ghost tets, BRIO/Hilbert order and a verifier

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh: Delaunay benchmark, hash-paired cavity faces, mesh.delaunay.points debug command

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh: simplify Delaunay helpers, share predicates and parsers, halve verifier work

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:
authored and GitHub committed 2026-10-13 15:56:42 -04:00
1 parent 2b5b47930f
commit 94dc50bb0c
13 files changed
+1508 -17

No files matched your search

+52 -1
View File
@@ -1,8 +1,9 @@
use modelcraft_mesh::delaunay::Triangulation;
use modelcraft_mesh::quality::{self, QualityReport};
use modelcraft_model::{ElementKind, MAX_ELEMENTS};
use serde_json::{Value, json};
use super::{label, labels, xyz};
use super::{label, labels, points, xyz};
use crate::{CommandSpec, EngineError, Result, Session};
pub const NODE_ADD: CommandSpec = CommandSpec {
@@ -32,6 +33,30 @@ pub const QUALITY: CommandSpec = CommandSpec {
run: quality_report,
};
pub const DELAUNAY_POINTS: CommandSpec = CommandSpec {
id: "mesh.delaunay.points",
label: "Delaunay of Points",
params: r#"{"points": [[x, y, z], ...] (at most 20000)} — the exact Delaunay tetrahedralization (a debugging report; the model is not changed): {"dimension", "vertices", "duplicates": [[point, kept point], ...], "tets": [[i, j, k, l], ...]} with 0-based point indices in tet4 order; collinear or coplanar points give no tets"#,
run: delaunay_points,
};
/// The most points `mesh.delaunay.points` takes (its report lists every tetrahedron).
const DELAUNAY_MAX_POINTS: usize = 20_000;
fn delaunay_points(_: &mut Session, p: &Value) -> Result<Value> {
let points = points(p, "points", DELAUNAY_MAX_POINTS)?;
let tri = Triangulation::new(&points)?;
tri.verify()?;
let duplicates: Vec<[u32; 2]> = (0..points.len() as u32).filter_map(|v| tri.duplicate_of(v).map(|d| [v, d])).collect();
let tets: Vec<[u32; 4]> = tri.finite_tets().map(|(_, vs)| vs).collect();
Ok(json!({
"dimension": tri.dimension(),
"vertices": points.len() - duplicates.len(),
"duplicates": duplicates,
"tets": tets,
}))
}
fn node_add(s: &mut Session, p: &Value) -> Result<Value> {
let l = label(p, "label")?;
s.model.set_node(l, xyz(p, "xyz")?)?;
@@ -127,6 +152,32 @@ mod tests {
s.execute("mesh.element.add", &json!({"label": label, "kind": "hex8", "nodes": nodes})).unwrap();
}
#[test]
fn delaunay_of_points_reports_tets_and_duplicates() {
let mut s = Session::default();
let cube = json!({"points": [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1], [1, 1, 1]]});
let r = s.execute("mesh.delaunay.points", &cube).unwrap();
assert_eq!(r["dimension"], 3);
assert_eq!(r["vertices"], 8);
assert_eq!(r["duplicates"], json!([[8, 7]]));
let tets = r["tets"].as_array().unwrap();
assert!((5..=6).contains(&tets.len()), "{tets:?}");
assert!(s.journal.is_empty(), "a report does not change the model");
let flat = s.execute("mesh.delaunay.points", &json!({"points": [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]})).unwrap();
assert_eq!((flat["dimension"].clone(), flat["tets"].clone()), (json!(2), json!([])));
for bad in [
json!({}),
json!({"points": [[0, 0]]}),
json!({"points": [[0, 0, "x"]]}),
json!({"points": [[f64::MAX, 0, 0]]}),
json!({"points": [[1e-300, 0, 0]]}),
json!({"points": vec![[0, 0, 0]; 20_001]}),
] {
assert!(s.execute("mesh.delaunay.points", &bad).is_err(), "{bad}");
}
}
#[test]
fn reports_quality_and_inverted_elements() {
let mut s = Session::default();