use crate::topology::BrepSolid;
use crate::{solid_scale, AnalyticSurface, KernelTolerances, Vec3};
use rustc_hash::FxHashMap as HashMap;
const HEAL_K: f64 = 1e-5;
const ACTIVATION_FLOOR: f64 = 1e-9;
const FEATURE_CAP: f64 = 0.25;
const PLANE_ANCHOR: f64 = 1e-6;
fn visit_heal_pairs(points: &[Vec3], heal_tol: f64, mut visit: impl FnMut(usize, usize)) {
let brute_force = |visit: &mut dyn FnMut(usize, usize)| {
for i in 0..points.len() {
for j in i + 1..points.len() {
visit(i, j);
}
}
};
let width = 2.0 * heal_tol;
if points.len() <= 32 || heal_tol < 1e-150 || !width.is_finite() || width <= 0.0 {
brute_force(&mut visit);
return;
}
let keys: Option<Vec<[i64; 3]>> = points
.iter()
.map(|point| {
let q = [point.x / width, point.y / width, point.z / width];
q.iter()
.all(|v| v.is_finite() && v.abs() <= (1u64 << 48) as f64)
.then(|| q.map(|v| v.floor() as i64))
})
.collect();
let Some(keys) = keys else {
brute_force(&mut visit);
return;
};
let mut cells: HashMap<[i64; 3], Vec<usize>> = HashMap::default();
for (i, key) in keys.iter().enumerate() {
cells.entry(*key).or_default().push(i);
}
let mut candidates = Vec::new();
for (i, &[x, y, z]) in keys.iter().enumerate() {
candidates.clear();
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
if let Some(indices) = cells.get(&[x + dx, y + dy, z + dz]) {
candidates.extend(indices.iter().copied().filter(|&j| j > i));
}
}
}
}
candidates.sort_unstable();
for &j in &candidates {
visit(i, j);
}
}
}
pub(crate) fn heal_operands(
solid: &mut BrepSolid,
policy: &KernelTolerances,
) -> Result<(), String> {
if solid.vertices.len() < 2 {
return Ok(());
}
let diagonal = solid_scale(solid);
let heal_tol = policy.heal_band(diagonal, HEAL_K);
if !(heal_tol > 0.0) || !heal_tol.is_finite() {
return Ok(());
}
let original = solid.clone();
let before = original.validate_with_tolerances(policy).len();
let moved = heal_operands_inner(solid, heal_tol)?;
let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
if !moved {
if debug {
eprintln!("heal: no-op (0 vertices moved, heal_tol={heal_tol:.3e})");
}
return Ok(());
}
if debug {
let count = original
.vertices
.iter()
.zip(&solid.vertices)
.filter(|(a, b)| a.point.sub(b.point).length() > 0.0)
.count();
eprintln!("heal: moved {count} vertices (heal_tol={heal_tol:.3e})");
}
let after = solid.validate_with_tolerances(policy).len();
if after > before {
if debug {
eprintln!("heal: discarded (validate worsened {before} -> {after})");
}
*solid = original;
return Ok(());
}
if debug {
oracle_scan(solid, heal_tol);
}
Ok(())
}
fn heal_operands_inner(solid: &mut BrepSolid, heal_tol: f64) -> Result<bool, String> {
let n = solid.vertices.len();
let planes_by_vertex = incident_planes(solid);
let shortest_edge = shortest_incident_edges(solid);
let ids: Vec<u64> = solid.vertices.iter().map(|v| v.id).collect();
let points: Vec<Vec3> = solid.vertices.iter().map(|v| v.point).collect();
let index_of: HashMap<u64, usize> = ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
let mut forbidden: rustc_hash::FxHashSet<(usize, usize)> = rustc_hash::FxHashSet::default();
for edge in &solid.edges {
if edge.start_vertex_id == edge.end_vertex_id {
continue;
}
let (Some(&a), Some(&b)) = (
index_of.get(&edge.start_vertex_id),
index_of.get(&edge.end_vertex_id),
) else {
continue;
};
let length = points[a].sub(points[b]).length();
if !edge.degenerate && length > heal_tol {
forbidden.insert((a.min(b), a.max(b)));
}
}
let mut parent: Vec<usize> = (0..n).collect();
fn find(parent: &mut Vec<usize>, mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
let caps: Vec<f64> = ids
.iter()
.map(|id| {
shortest_edge
.get(id)
.map(|len| len * FEATURE_CAP)
.unwrap_or(f64::INFINITY)
})
.collect();
visit_heal_pairs(&points, heal_tol, |i, j| {
if forbidden.contains(&(i, j)) {
return;
}
let radius = heal_tol.min(caps[i]).min(caps[j]);
if points[i].sub(points[j]).length() <= radius {
let a = find(&mut parent, i);
let b = find(&mut parent, j);
if a != b {
parent[a] = b;
}
}
});
let mut clusters: HashMap<usize, Vec<usize>> = HashMap::default();
for i in 0..n {
let root = find(&mut parent, i);
clusters.entry(root).or_default().push(i);
}
let mut new_point: Vec<Vec3> = points.clone();
let mut any_move = false;
for members in clusters.values() {
let base = members
.iter()
.fold(Vec3::default(), |acc, &m| acc.add(points[m]))
.scale(1.0 / members.len() as f64);
let mut planes: Vec<(Vec3, f64)> = Vec::new();
for &m in members {
if let Some(list) = planes_by_vertex.get(&ids[m]) {
for &(normal, offset) in list {
if !planes.iter().any(|(pn, pd)| {
pn.dot(normal).abs() > 1.0 - 1e-9 && (pd - offset).abs() <= heal_tol
}) {
planes.push((normal, offset));
}
}
}
}
let target = plane_snap(base, &planes);
let is_fuse = members.len() > 1;
let off_plane = planes
.iter()
.map(|(nrm, off)| (nrm.dot(base) - off).abs())
.fold(0.0_f64, f64::max);
if !is_fuse && off_plane <= ACTIVATION_FLOOR {
continue;
}
for &m in members {
if target.sub(points[m]).length() > heal_tol {
if is_fuse && base.sub(points[m]).length() <= heal_tol {
if base.sub(points[m]).length() > 0.0 {
new_point[m] = base;
any_move = true;
}
}
continue;
}
if target.sub(points[m]).length() > 0.0 {
new_point[m] = target;
any_move = true;
}
}
}
if !any_move {
return Ok(false);
}
for (i, vertex) in solid.vertices.iter_mut().enumerate() {
vertex.point = new_point[i];
}
crate::boolean::commit_nearby_edge_endpoints(solid, heal_tol)?;
Ok(true)
}
fn incident_planes(solid: &BrepSolid) -> HashMap<u64, Vec<(Vec3, f64)>> {
let edge_vertices: HashMap<u64, (u64, u64)> = solid
.edges
.iter()
.map(|edge| (edge.id, (edge.start_vertex_id, edge.end_vertex_id)))
.collect();
let mut out: HashMap<u64, Vec<(Vec3, f64)>> = HashMap::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
let Some(AnalyticSurface::Plane {
origin,
u_dir,
v_dir,
..
}) = face.surface.analytic()
else {
continue;
};
let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
continue;
};
let offset = normal.dot(*origin);
let mut touched: rustc_hash::FxHashSet<u64> = rustc_hash::FxHashSet::default();
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
if let Some(&(start, end)) = edge_vertices.get(&coedge.edge_id) {
touched.insert(start);
touched.insert(end);
}
}
}
for vertex_id in touched {
out.entry(vertex_id).or_default().push((normal, offset));
}
}
out
}
fn shortest_incident_edges(solid: &BrepSolid) -> HashMap<u64, f64> {
let points: HashMap<u64, Vec3> = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect();
let mut out: HashMap<u64, f64> = HashMap::default();
for edge in &solid.edges {
if edge.degenerate || edge.start_vertex_id == edge.end_vertex_id {
continue;
}
let (Some(&a), Some(&b)) = (
points.get(&edge.start_vertex_id),
points.get(&edge.end_vertex_id),
) else {
continue;
};
let length = a.sub(b).length();
for id in [edge.start_vertex_id, edge.end_vertex_id] {
let slot = out.entry(id).or_insert(f64::INFINITY);
if length < *slot {
*slot = length;
}
}
}
out
}
fn plane_snap(base: Vec3, planes: &[(Vec3, f64)]) -> Vec3 {
if planes.is_empty() {
return base;
}
let mut matrix = [[0.0f64; 3]; 3];
let mut rhs = [0.0f64; 3];
for i in 0..3 {
matrix[i][i] = PLANE_ANCHOR;
}
let base_arr = [base.x, base.y, base.z];
for i in 0..3 {
rhs[i] = PLANE_ANCHOR * base_arr[i];
}
for (normal, offset) in planes {
let na = [normal.x, normal.y, normal.z];
for i in 0..3 {
for j in 0..3 {
matrix[i][j] += na[i] * na[j];
}
rhs[i] += offset * na[i];
}
}
match crate::fit::solve_small(matrix, rhs, 3) {
Ok(solution) => Vec3::new(solution[0], solution[1], solution[2]),
Err(_) => base,
}
}
fn oracle_scan(solid: &BrepSolid, heal_tol: f64) {
let vs = &solid.vertices;
for i in 0..vs.len() {
for j in (i + 1)..vs.len() {
let gap = vs[i].point.sub(vs[j].point).length();
if gap <= heal_tol {
eprintln!(
"heal oracle: vertices {} and {} still within heal_tol ({:.3e})",
vs[i].id, vs[j].id, gap
);
}
}
}
}