use crate::{KernelRefusal, KernelStage, OrRefuse};
use super::*;
fn face_normal(fragment: &FaceFragmentRecord) -> Result<Vec3, KernelRefusal> {
let normal = fragment
.surface
.normal(fragment.test_uv.x, fragment.test_uv.y).or_refuse(KernelStage::Select, "normal")?;
Ok(if fragment.same_sense {
normal
} else {
normal.scale(-1.0)
})
}
fn classify_fragment(
classifier: &SolidClassifier,
fragment: &FaceFragmentRecord,
debug: bool,
) -> Result<crate::PointClassification, KernelRefusal> {
let primary = classifier.classify(fragment.test_point).or_refuse(KernelStage::Select, "classify")?;
if debug {
eprintln!(
"frag src_face={} test=({:.6},{:.6},{:.6}) primary={:?} extras={}",
fragment.source_face_id,
fragment.test_point.x,
fragment.test_point.y,
fragment.test_point.z,
primary.class,
fragment.extra_test_points.len()
);
}
if primary.class == PointClass::On {
let mut in_votes = 0usize;
let mut out_votes = 0usize;
let mut on_votes = 0usize;
for &point in &fragment.extra_test_points {
match classifier.classify(point).or_refuse(KernelStage::Select, "classify")?.class {
PointClass::On => on_votes += 1,
PointClass::In => in_votes += 1,
PointClass::Out => out_votes += 1,
}
}
if on_votes == 0 && (in_votes >= 2 || out_votes >= 2) {
for &extra in &fragment.extra_test_points {
let step = fragment
.test_point
.add(extra.sub(fragment.test_point).scale(0.05));
if classifier.classify(step).or_refuse(KernelStage::Select, "classify")?.class == PointClass::On {
on_votes += 1;
break;
}
}
}
if on_votes == 0 && in_votes == 0 && out_votes >= 2 {
if debug {
eprintln!(
"frag src_face={} tangential On overturned -> Out ({out_votes} extras)",
fragment.source_face_id
);
}
return Ok(crate::PointClassification {
class: PointClass::Out,
on_normal: None,
});
}
if on_votes == 0 && out_votes == 0 && in_votes >= 2 {
if debug {
eprintln!(
"frag src_face={} tangential On overturned -> In ({in_votes} extras)",
fragment.source_face_id
);
}
return Ok(crate::PointClassification {
class: PointClass::In,
on_normal: None,
});
}
return Ok(primary);
}
if let Some(on_normal) = classifier.coincident_on_normal(fragment.test_point).or_refuse(KernelStage::Select, "coincident_on_normal")? {
if debug {
eprintln!(
"frag src_face={} coincident-On rescue {:?} -> On",
fragment.source_face_id, primary.class
);
}
return Ok(crate::PointClassification {
class: PointClass::On,
on_normal: Some(on_normal),
});
}
if fragment.extra_test_points.is_empty() {
return Ok(primary);
}
let mut agreeing = 0usize;
let mut opposing = 0usize;
for &point in &fragment.extra_test_points {
match classifier.classify(point).or_refuse(KernelStage::Select, "classify")?.class {
class if class == primary.class => agreeing += 1,
PointClass::On => {}
_ => opposing += 1,
}
}
if opposing >= 2 && agreeing == 0 {
let overturned = match primary.class {
PointClass::In => PointClass::Out,
_ => PointClass::In,
};
if debug {
eprintln!(
"frag src_face={} vote overturned {:?} -> {:?} ({} opposing)",
fragment.source_face_id, primary.class, overturned, opposing
);
}
return Ok(crate::PointClassification {
class: overturned,
on_normal: None,
});
}
Ok(primary)
}
fn boundary_coedge_key(
fragment: &FaceFragmentRecord,
edge_id: u64,
pcurve: &crate::NurbsCurve,
quantum: f64,
) -> Option<(u64, [i64; 3])> {
let [start, end] = pcurve.domain().ok()?;
let uv = pcurve.evaluate((start + end) * 0.5).ok()?;
let point = fragment.surface.evaluate(uv.x, uv.y).ok()?;
Some((
edge_id,
[
(point.x / quantum).round() as i64,
(point.y / quantum).round() as i64,
(point.z / quantum).round() as i64,
],
))
}
fn side_keep_decisions(
fragments: &[FaceFragmentRecord],
classifier: &SolidClassifier,
first_operand: bool,
operation: BooleanOperation,
tolerance: f64,
debug: bool,
barrier_edges: &HashSet<(u8, u64)>,
) -> Result<Vec<bool>, KernelRefusal> {
let decide = |fragment: &FaceFragmentRecord,
classification: &crate::PointClassification|
-> Result<(bool, bool), KernelRefusal> {
if debug {
eprintln!(
"frag {} src_face={} test=({:.6},{:.6},{:.6}) class={:?}",
if first_operand { "A" } else { "B" },
fragment.source_face_id,
fragment.test_point.x,
fragment.test_point.y,
fragment.test_point.z,
classification.class,
);
}
let propagatable = classification.class != PointClass::On;
let keep = if first_operand {
match classification.class {
PointClass::On => {
let same = classification
.on_normal
.ok_or_else(|| {
"boolean selection: boundary classification lacks normal".to_string()
}).or_refuse(KernelStage::Select, "csg.boolean.select")?
.dot(face_normal(fragment)?)
> 0.0;
match operation {
BooleanOperation::Subtract => !same,
BooleanOperation::Union | BooleanOperation::Intersect => same,
}
}
PointClass::In => matches!(operation, BooleanOperation::Intersect),
PointClass::Out => !matches!(operation, BooleanOperation::Intersect),
}
} else {
match classification.class {
PointClass::On => false,
PointClass::Out => matches!(operation, BooleanOperation::Union),
PointClass::In => matches!(
operation,
BooleanOperation::Intersect | BooleanOperation::Subtract
),
}
};
Ok((keep, propagatable))
};
let mut keep: Vec<Option<bool>> = vec![None; fragments.len()];
let mut propagatable: Vec<bool> = vec![false; fragments.len()];
for (index, fragment) in fragments.iter().enumerate() {
let touched = fragment
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| !matches!(coedge.source, FragmentEdgeSource::Boundary { .. }));
if touched || classifier.near_surface(fragment.test_point).or_refuse(KernelStage::Select, "near_surface")? {
let classification = classify_fragment(classifier, fragment, debug)?;
let (value, source) = decide(fragment, &classification)?;
keep[index] = Some(value);
propagatable[index] = source;
}
}
let quantum = tolerance.max(1e-5) * 10.0;
let mut edge_fragments: HashMap<(u64, [i64; 3]), Vec<usize>> = HashMap::default();
for (index, fragment) in fragments.iter().enumerate() {
for coedge in fragment
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
if let FragmentEdgeSource::Boundary { edge_id, .. } = coedge.source {
let fragment_operand = if first_operand { 0u8 } else { 1u8 };
if barrier_edges.contains(&(fragment_operand, edge_id)) {
continue;
}
if let Some(key) = boundary_coedge_key(fragment, edge_id, &coedge.pcurve, quantum) {
let entry = edge_fragments.entry(key).or_default();
if !entry.contains(&index) {
entry.push(index);
}
}
}
}
}
let mut on_adjacent: Vec<bool> = vec![false; fragments.len()];
for neighbors in edge_fragments.values() {
if neighbors
.iter()
.any(|&index| keep[index].is_some() && !propagatable[index])
{
for &index in neighbors {
on_adjacent[index] = true;
}
}
}
for index in 0..fragments.len() {
if on_adjacent[index] && keep[index].is_none() {
let classification = classify_fragment(classifier, &fragments[index], debug)?;
let (value, source) = decide(&fragments[index], &classification)?;
keep[index] = Some(value);
propagatable[index] = source;
}
}
loop {
let mut progressed = false;
for neighbors in edge_fragments.values() {
let mut fate: Option<(bool, usize)> = None;
for &index in neighbors {
if !propagatable[index] {
continue;
}
if let Some(value) = keep[index] {
if let Some((known, source)) = fate {
if known != value {
if debug {
eprintln!(
"frag {} propagation conflict (faces {} vs {}) — \
falling back to direct classification",
if first_operand { "A" } else { "B" },
fragments[source].source_face_id,
fragments[index].source_face_id,
);
}
let mut direct = Vec::with_capacity(fragments.len());
for fragment in fragments {
let classification =
classify_fragment(classifier, fragment, debug)?;
direct.push(decide(fragment, &classification)?.0);
}
return Ok(direct);
}
} else {
fate = Some((value, index));
}
}
}
if let Some((value, _)) = fate {
for &index in neighbors {
if keep[index].is_none() {
keep[index] = Some(value);
propagatable[index] = true;
if debug {
eprintln!(
"frag {} src_face={} inherited keep={value} by adjacency",
if first_operand { "A" } else { "B" },
fragments[index].source_face_id,
);
}
progressed = true;
}
}
}
}
if !progressed {
match keep.iter().position(Option::is_none) {
Some(index) => {
let classification = classify_fragment(classifier, &fragments[index], debug)?;
let (value, source) = decide(&fragments[index], &classification)?;
keep[index] = Some(value);
propagatable[index] = source;
}
None => break,
}
}
}
Ok(keep
.into_iter()
.map(|value| value.unwrap_or(false))
.collect())
}
pub(super) fn select_fragments(
fragments_a: Vec<FaceFragmentRecord>,
fragments_b: Vec<FaceFragmentRecord>,
pristine_a: &BrepSolid,
pristine_b: &BrepSolid,
operation: BooleanOperation,
tolerance: f64,
barrier_edges: &HashSet<(u8, u64)>,
) -> Result<Vec<FaceFragmentRecord>, KernelRefusal> {
let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
let mut selected = Vec::new();
let classifier_b = SolidClassifier::new(pristine_b, tolerance).or_refuse(KernelStage::Select, "new")?;
let classifier_a = SolidClassifier::new(pristine_a, tolerance).or_refuse(KernelStage::Select, "new")?;
let keep_a = side_keep_decisions(
&fragments_a,
&classifier_b,
true,
operation,
tolerance,
debug,
barrier_edges,
)?;
for (fragment, keep) in fragments_a.into_iter().zip(keep_a) {
if keep {
selected.push(fragment);
}
}
let keep_b = side_keep_decisions(
&fragments_b,
&classifier_a,
false,
operation,
tolerance,
debug,
barrier_edges,
)?;
for (mut fragment, keep) in fragments_b.into_iter().zip(keep_b) {
if !keep {
continue;
}
if matches!(operation, BooleanOperation::Subtract) {
fragment.same_sense = !fragment.same_sense;
for loop_record in &mut fragment.loops {
loop_record.coedges.reverse();
for coedge in &mut loop_record.coedges {
coedge.forward = !coedge.forward;
coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Select, "reversed")?;
}
}
}
selected.push(fragment);
}
Ok(selected)
}
enum NaryKeep {
Drop,
AsIs,
Reversed,
}
fn nary_keep(
classifiers: &[SolidClassifier],
owner: usize,
fragment: &FaceFragmentRecord,
operation: BooleanOperation,
debug: bool,
) -> Result<NaryKeep, KernelRefusal> {
let n = classifiers.len();
let outward = |fragment: &FaceFragmentRecord| face_normal(fragment);
match operation {
BooleanOperation::Union => {
for other in 0..n {
if other == owner {
continue;
}
let class = classify_fragment(&classifiers[other], fragment, debug)?;
match class.class {
PointClass::In => return Ok(NaryKeep::Drop),
PointClass::On => {
if other < owner {
return Ok(NaryKeep::Drop);
}
let same = class
.on_normal
.ok_or_else(|| {
"n-ary selection: On classification lacks normal".to_string()
}).or_refuse(KernelStage::Select, "csg.boolean.select")?
.dot(outward(fragment)?)
> 0.0;
if !same {
return Ok(NaryKeep::Drop);
}
}
PointClass::Out => {}
}
}
Ok(NaryKeep::AsIs)
}
BooleanOperation::Intersect => {
for other in 0..n {
if other == owner {
continue;
}
let class = classify_fragment(&classifiers[other], fragment, debug)?;
match class.class {
PointClass::Out => return Ok(NaryKeep::Drop),
PointClass::On => {
if other < owner {
return Ok(NaryKeep::Drop);
}
let same = class
.on_normal
.ok_or_else(|| {
"n-ary selection: On classification lacks normal".to_string()
}).or_refuse(KernelStage::Select, "csg.boolean.select")?
.dot(outward(fragment)?)
> 0.0;
if !same {
return Ok(NaryKeep::Drop);
}
}
PointClass::In => {}
}
}
Ok(NaryKeep::AsIs)
}
BooleanOperation::Subtract => {
if owner == 0 {
for cutter in 1..n {
let class = classify_fragment(&classifiers[cutter], fragment, debug)?;
match class.class {
PointClass::In => return Ok(NaryKeep::Drop),
PointClass::On => {
let same = class
.on_normal
.ok_or_else(|| {
"n-ary selection: On classification lacks normal".to_string()
}).or_refuse(KernelStage::Select, "csg.boolean.select")?
.dot(outward(fragment)?)
> 0.0;
if same {
return Ok(NaryKeep::Drop);
}
}
PointClass::Out => {}
}
}
Ok(NaryKeep::AsIs)
} else {
let base = classify_fragment(&classifiers[0], fragment, debug)?;
match base.class {
PointClass::Out | PointClass::On => return Ok(NaryKeep::Drop),
PointClass::In => {}
}
for other in 1..n {
if other == owner {
continue;
}
let class = classify_fragment(&classifiers[other], fragment, debug)?;
match class.class {
PointClass::In => return Ok(NaryKeep::Drop),
PointClass::On => {
if other < owner {
return Ok(NaryKeep::Drop);
}
let same = class
.on_normal
.ok_or_else(|| {
"n-ary selection: On classification lacks normal".to_string()
}).or_refuse(KernelStage::Select, "csg.boolean.select")?
.dot(outward(fragment)?)
> 0.0;
if !same {
return Ok(NaryKeep::Drop);
}
}
PointClass::Out => {}
}
}
Ok(NaryKeep::Reversed)
}
}
}
}
pub(super) fn select_fragments_nary(
fragments: &[Vec<FaceFragmentRecord>],
pristine: &[BrepSolid],
operation: BooleanOperation,
tolerance: f64,
debug: bool,
) -> Result<Vec<FaceFragmentRecord>, KernelRefusal> {
let classifiers = pristine
.iter()
.map(|solid| SolidClassifier::new(solid, tolerance))
.collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Select, "csg.boolean.select")?;
let mut selected = Vec::new();
for (owner, owner_fragments) in fragments.iter().enumerate() {
for fragment in owner_fragments {
match nary_keep(&classifiers, owner, fragment, operation, debug)? {
NaryKeep::Drop => {}
NaryKeep::AsIs => selected.push(fragment.clone()),
NaryKeep::Reversed => {
let mut reversed = fragment.clone();
reverse_fragment(&mut reversed)?;
selected.push(reversed);
}
}
}
}
Ok(selected)
}