use super::boolean_vids;
use crate::kernel::arrangement::{arrange, Arrangement, BoolOp, Tri};
use crate::kernel::interner::Vid;
use crate::kernel::mesh_bridge::{mesh_to_tris, orient_outward};
use crate::mesh::Mesh;
use nalgebra::{Point3, Rotation3, Unit, Vector3};
use std::collections::{BTreeSet, HashMap, HashSet};
fn boxed(min: [f64; 3], size: [f64; 3], rot: Option<(Vector3<f64>, f64, [f64; 3])>) -> Mesh {
let mx = [min[0] + size[0], min[1] + size[1], min[2] + size[2]];
let c = |i: usize| -> [f64; 2] { [min[i], mx[i]] };
let mut corners: Vec<Point3<f64>> = [
(0, 0, 0),
(1, 0, 0),
(1, 1, 0),
(0, 1, 0),
(0, 0, 1),
(1, 0, 1),
(1, 1, 1),
(0, 1, 1),
]
.iter()
.map(|&(i, j, k)| Point3::new(c(0)[i], c(1)[j], c(2)[k]))
.collect();
if let Some((axis, angle, about)) = rot {
let r = Rotation3::from_axis_angle(&Unit::new_normalize(axis), angle);
let o = Point3::new(about[0], about[1], about[2]);
for p in corners.iter_mut() {
*p = o + r * (*p - o);
}
}
let faces: [[usize; 4]; 6] = [
[0, 3, 2, 1],
[4, 5, 6, 7],
[0, 1, 5, 4],
[2, 3, 7, 6],
[0, 4, 7, 3],
[1, 2, 6, 5],
];
let mut m = Mesh::with_capacity(24, 36);
for f in &faces {
let e1 = corners[f[1]] - corners[f[0]];
let e2 = corners[f[2]] - corners[f[0]];
let n = e1.cross(&e2).try_normalize(1e-12).unwrap_or(Vector3::z());
let b = m.vertex_count() as u32;
for &i in f {
m.add_vertex(corners[i], n);
}
m.add_triangle(b, b + 1, b + 2);
m.add_triangle(b, b + 2, b + 3);
}
m
}
fn sweep_261_operands() -> (Mesh, Mesh) {
let a_min = [-1.72371594746207, -0.35246108913603935, -1.2204342720208154];
let a_size = [2.8534163464770894, 3.0795194627753784, 2.858202766048261];
let b_min = [-2.5947221996202225, 0.7995282321488091, -1.1895637752048271];
let b_size = [3.215043208338911, 0.9570224289084479, 3.548848436777412];
let axis = [0.413429423622099, -0.8221765971936017, -0.6789513492042303];
let angle = 1.3791241095493956;
let a = boxed(a_min, a_size, None);
let about = [
b_min[0] + b_size[0] / 2.0,
b_min[1] + b_size[1] / 2.0,
b_min[2] + b_size[2] / 2.0,
];
let b = boxed(
b_min,
b_size,
Some((Vector3::new(axis[0], axis[1], axis[2]), angle, about)),
);
(a, b)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Origin {
A,
B,
Unresolved,
Both,
}
fn origin_of(tri: [Vid; 3], tris_a: &HashSet<[Vid; 3]>, tris_b: &HashSet<[Vid; 3]>) -> Origin {
match (tris_a.contains(&tri), tris_b.contains(&tri)) {
(true, false) => Origin::A,
(false, true) => Origin::B,
(true, true) => Origin::Both,
(false, false) => Origin::Unresolved,
}
}
fn dist2(p: [f64; 3], q: [f64; 3]) -> f64 {
let d = super::sub_f64(p, q);
super::dot3(d, d)
}
fn closest_point_on_segment3(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
let ab = super::sub_f64(b, a);
let len2 = super::dot3(ab, ab);
if len2 == 0.0 {
return a;
}
let ap = super::sub_f64(p, a);
let t = (super::dot3(ap, ab) / len2).clamp(0.0, 1.0);
[a[0] + t * ab[0], a[1] + t * ab[1], a[2] + t * ab[2]]
}
fn closest_point_on_triangle3(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
let ab = super::sub_f64(b, a);
let ac = super::sub_f64(c, a);
let n = super::cross3(ab, ac);
let area2 = super::dot3(n, n);
let scale2 = super::dot3(ab, ab).max(super::dot3(ac, ac)).max(1.0);
if area2 <= 1e-24 * scale2 * scale2 {
let cands = [
closest_point_on_segment3(p, a, b),
closest_point_on_segment3(p, b, c),
closest_point_on_segment3(p, c, a),
];
let mut best = cands[0];
for cand in cands.iter().skip(1) {
if dist2(p, *cand) < dist2(p, best) {
best = *cand;
}
}
return best;
}
let ap = super::sub_f64(p, a);
let d1 = super::dot3(ab, ap);
let d2 = super::dot3(ac, ap);
if d1 <= 0.0 && d2 <= 0.0 {
return a;
}
let bp = super::sub_f64(p, b);
let d3 = super::dot3(ab, bp);
let d4 = super::dot3(ac, bp);
if d3 >= 0.0 && d4 <= d3 {
return b;
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = d1 / (d1 - d3);
return [a[0] + v * ab[0], a[1] + v * ab[1], a[2] + v * ab[2]];
}
let cp = super::sub_f64(p, c);
let d5 = super::dot3(ab, cp);
let d6 = super::dot3(ac, cp);
if d6 >= 0.0 && d5 <= d6 {
return c;
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = d2 / (d2 - d6);
return [a[0] + w * ac[0], a[1] + w * ac[1], a[2] + w * ac[2]];
}
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
return [
b[0] + w * (c[0] - b[0]),
b[1] + w * (c[1] - b[1]),
b[2] + w * (c[2] - b[2]),
];
}
let denom = 1.0 / (va + vb + vc);
let v = vb * denom;
let w = vc * denom;
[
a[0] + v * ab[0] + w * ac[0],
a[1] + v * ab[1] + w * ac[1],
a[2] + v * ab[2] + w * ac[2],
]
}
fn min_point_to_mesh_distance(p: [f64; 3], mesh: &[Tri]) -> f64 {
mesh.iter()
.map(|t| dist2(p, closest_point_on_triangle3(p, t[0], t[1], t[2])))
.fold(f64::INFINITY, f64::min)
.sqrt()
}
fn edge_census(
kept: &[[Vid; 3]],
tris_a: &HashSet<[Vid; 3]>,
tris_b: &HashSet<[Vid; 3]>,
) -> HashMap<(Vid, Vid), Vec<(usize, Origin)>> {
let mut edges: HashMap<(Vid, Vid), Vec<(usize, Origin)>> = HashMap::new();
for (idx, tri) in kept.iter().enumerate() {
let origin = origin_of(*tri, tris_a, tris_b);
for k in 0..3 {
let (u, v) = (tri[k], tri[(k + 1) % 3]);
let key = (u.min(v), u.max(v));
edges.entry(key).or_default().push((idx, origin));
}
}
edges
}
#[test]
fn sweep_261_kept_triangles_are_nonmanifold_in_vid_space() {
let (mesh_a, mesh_b) = sweep_261_operands();
crate::kernel::budget::begin();
let a: Vec<Tri> = orient_outward(mesh_to_tris(&mesh_a));
let b: Vec<Tri> = orient_outward(mesh_to_tris(&mesh_b));
let arr: Arrangement = arrange(&a, &b);
assert_eq!(
arr.unrecovered, 0,
"sweep_261's arrangement is expected to fully recover every constraint \
(the documented premise of the #3353 classification-level tear — if this \
now fails, the defect has moved from classification into arrangement \
conformity, and `issue_3353_unrecovered_crosstab.rs` is the file to \
extend, not this one)"
);
let kept: Vec<[Vid; 3]> = boolean_vids(&arr, &a, &b, BoolOp::Union);
assert!(!kept.is_empty(), "sweep_261's union must keep at least one triangle");
let tris_a: HashSet<[Vid; 3]> = arr.tris_a.iter().copied().collect();
let tris_b: HashSet<[Vid; 3]> = arr.tris_b.iter().copied().collect();
for tri in &kept {
assert_ne!(
origin_of(*tri, &tris_a, &tris_b),
Origin::Unresolved,
"kept triangle {tri:?} is not verbatim present in arr.tris_a or \
arr.tris_b — the Union-only \"never flipped\" assumption in \
`origin_of`'s doc comment does not hold for this triangle; \
investigate before trusting any origin tag in this test's output"
);
}
let census = edge_census(&kept, &tris_a, &tris_b);
let mut overused: Vec<(&(Vid, Vid), &Vec<(usize, Origin)>)> =
census.iter().filter(|(_, users)| users.len() != 2).collect();
overused.sort_by_key(|(edge, _)| **edge);
println!("sweep_261 kept-triangle Vid set ({} triangles):", kept.len());
for (idx, tri) in kept.iter().enumerate() {
println!(
" [{idx}] {tri:?} origin={:?}",
origin_of(*tri, &tris_a, &tris_b)
);
}
println!(
"edges with multiplicity != 2 ({} of {} total kept-triangle edges):",
overused.len(),
census.len()
);
for (edge, users) in &overused {
println!(" {edge:?} used {} time(s) by:", users.len());
for (idx, origin) in users.iter() {
println!(" kept[{idx}] = {:?} origin={:?}", kept[*idx], origin);
}
}
let implicated: BTreeSet<usize> = overused
.iter()
.flat_map(|(_, users)| users.iter().map(|(idx, _)| *idx))
.collect();
println!(
"centroid-to-opposite-surface distance, {} kept triangle(s) on an over-used edge:",
implicated.len()
);
for idx in implicated {
let tri = kept[idx];
let origin = origin_of(tri, &tris_a, &tris_b);
let c = super::centroid(&arr, tri);
let mag = super::dot3(c, c).sqrt();
let report = |label: &str, surface: &[Tri]| {
let d = min_point_to_mesh_distance(c, surface);
let relative = if mag > 0.0 { d / mag } else { f64::NAN };
println!(
" kept[{idx}] {tri:?} origin={origin:?} dist_to_{label}={d:.6e} \
relative={relative:.3e} |centroid|={mag:.6e}"
);
};
match origin {
Origin::A => report("b", &b),
Origin::B => report("a", &a),
Origin::Both => {
report("a", &a);
report("b", &b);
}
Origin::Unresolved => unreachable!("ruled out by the guard above"),
}
}
assert!(
!overused.is_empty(),
"expected sweep_261's kept-triangle set to be non-manifold in Vid space \
(issue #3353's classification-level tear) but every edge had multiplicity \
2 — either the defect is fixed (in which case: un-ignore \
`issue_3353_sweep_261_classification_tear.rs` too, and delete or repurpose \
this test) or this file's reproduction no longer matches the documented \
case and needs re-diagnosing before trusting either outcome"
);
}