use super::*;
use crate::camera::Aabb;
const MAX_BOOLEAN_PAIRS: usize = 64;
const VOLUME_EPSILON: f64 = 1e-6;
#[derive(Debug, Clone, PartialEq)]
pub struct InterferencePair {
pub a: String,
pub b: String,
pub volume: f64,
pub a_hidden: bool,
pub b_hidden: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct InterferenceReport {
pub component_count: usize,
pub pair_total: usize,
pub booleans_run: usize,
pub pairs: Vec<InterferencePair>,
pub skipped: Vec<String>,
pub unverified: Vec<String>,
}
fn note_boolean_call() {
}
fn intersect_volume(a: u32, b: u32) -> Result<f64, String> {
note_boolean_call();
let result = brep_kernel::boolean_handle_native(
a,
b,
brep_kernel::BooleanOperation::Intersect,
&brep_kernel::BooleanOptions::default(),
)?;
let volume = brep_kernel::mass_properties_handle_native(result, 1.0).map(|p| p.volume);
brep_kernel::free_solid(result);
volume
}
pub(crate) struct PlanComponent {
pub id: String,
pub bbox: Aabb,
pub hidden: bool,
}
pub(crate) struct PairPlan {
pub boolean_pairs: Vec<(usize, usize)>,
pub skipped: Vec<String>,
pub pair_total: usize,
}
fn inflated(bbox: &Aabb) -> Aabb {
if bbox.is_empty() {
return *bbox;
}
let size = bbox.size();
let diagonal = (size[0] * size[0] + size[1] * size[1] + size[2] * size[2]).sqrt();
let margin = (diagonal * 0.01).max(1e-6);
let mut out = *bbox;
for axis in 0..3 {
out.min[axis] -= margin;
out.max[axis] += margin;
}
out
}
fn overlaps(a: &Aabb, b: &Aabb) -> bool {
if a.is_empty() || b.is_empty() {
return false;
}
(0..3).all(|axis| a.min[axis] <= b.max[axis] && b.min[axis] <= a.max[axis])
}
pub(crate) fn plan_pairs(components: &[PlanComponent], cap: usize) -> PairPlan {
let mut plan = PairPlan {
boolean_pairs: Vec::new(),
skipped: Vec::new(),
pair_total: components.len().saturating_sub(1) * components.len() / 2,
};
for component in components {
if component.bbox.is_empty() {
plan.skipped
.push(format!("{} — no resident geometry, not checked", component.id));
}
}
let boxes: Vec<Aabb> = components.iter().map(|c| inflated(&c.bbox)).collect();
for i in 0..components.len() {
for j in (i + 1)..components.len() {
if !overlaps(&boxes[i], &boxes[j]) {
continue; }
if plan.boolean_pairs.len() >= cap {
plan.skipped.push(format!(
"{} × {} — skipped (boolean budget of {cap} pairs reached)",
components[i].id, components[j].id
));
continue;
}
plan.boolean_pairs.push((i, j));
}
}
plan
}
impl EngineState {
pub fn interference_check(&mut self) -> InterferenceReport {
self.ensure_assembly_synced();
let ids = self.component_ids();
let mut members: Vec<Vec<String>> = Vec::with_capacity(ids.len());
let mut plan_input: Vec<PlanComponent> = Vec::with_capacity(ids.len());
for id in &ids {
let info = self.component_info(id);
let solids = info.map(|info| info.members).unwrap_or_default();
let mut bbox = Aabb::empty();
let mut hidden = false;
for name in &solids {
if let Some(solid) = self.scene.solid(name) {
bbox.union(&solid.bbox);
hidden |= !solid.visible;
}
}
plan_input.push(PlanComponent {
id: id.clone(),
bbox,
hidden,
});
members.push(solids);
}
let plan = plan_pairs(&plan_input, MAX_BOOLEAN_PAIRS);
let mut report = InterferenceReport {
component_count: ids.len(),
pair_total: plan.pair_total,
booleans_run: 0,
pairs: Vec::new(),
skipped: plan.skipped,
unverified: Vec::new(),
};
if plan.boolean_pairs.is_empty() {
return report;
}
let handles = self.resident_solid_handles();
for (i, j) in plan.boolean_pairs {
report.booleans_run += 1;
let mut volume = 0.0;
let mut refusal: Option<String> = None;
for solid_a in &members[i] {
for solid_b in &members[j] {
let (Some(&ha), Some(&hb)) = (handles.get(solid_a), handles.get(solid_b))
else {
continue; };
let (Some(a), Some(b)) =
(self.scene.solid(solid_a), self.scene.solid(solid_b))
else {
continue;
};
if !overlaps(&inflated(&a.bbox), &inflated(&b.bbox)) {
continue;
}
match intersect_volume(ha, hb) {
Ok(v) => volume += v,
Err(error) => {
refusal.get_or_insert(error);
}
}
}
}
let (a, b) = (&plan_input[i], &plan_input[j]);
if let Some(error) = refusal {
report
.unverified
.push(format!("{} × {} — boolean refused: {error}", a.id, b.id));
}
if volume > VOLUME_EPSILON {
report.pairs.push(InterferencePair {
a: a.id.clone(),
b: b.id.clone(),
volume,
a_hidden: a.hidden,
b_hidden: b.hidden,
});
}
}
report
.pairs
.sort_by(|x, y| y.volume.total_cmp(&x.volume));
report
}
}