use crate::{KernelRefusal, KernelStage, OrRefuse, RefusalClass};
use super::*;
use super::builder::{face_name_in_solids, snap_edge_curve_endpoints, source_edge, AssembleIndex};
use super::edge_conform::{conform_overlapping_one_use_edges, edge_lies_on};
use super::refusal_welds::{refresh_derived_genus, weld_identical_coincident_one_use_edges, weld_tangency_duplicate_vertices};
fn assemble_fragments_impl(
selected: Vec<FaceFragmentRecord>,
solids: &HashMap<u8, &BrepSolid>,
imprint: &ImprintResultRecord,
tolerance: f64,
allow_open: bool,
) -> Result<BrepSolid, KernelRefusal> {
if selected.is_empty() {
return Err(KernelRefusal::new(
RefusalClass::ConservativeEmptyOverlap,
KernelStage::Sew,
"boolean assembly: operation produced no boundary faces",
));
}
let mut assembler = Assembler {
tolerance,
vertices: Vec::new(),
edges: Vec::new(),
edge_use_counts: HashMap::default(),
edge_first_forward: HashMap::default(),
edge_boundary_key: HashMap::default(),
next_vertex_id: 1,
next_edge_id: 1,
next_coedge_id: 1,
next_loop_id: 1,
next_face_id: 1,
};
let mut faces = Vec::new();
let mut used_face_names = HashSet::<String>::default();
let profile = std::env::var("BREP_PROFILE").is_ok();
let stage_started = Instant::now();
let index = AssembleIndex::build(solids, imprint);
let dbg_asm = std::env::var("BREP_DEBUG_ASM_EDGES").is_ok();
let mut dbg_edge_src: HashMap<u64, Vec<String>> = HashMap::default();
for fragment in selected {
let dbg_operand = fragment.operand;
let dbg_face = fragment.source_face_id;
let mut loops = Vec::new();
for fragment_loop in fragment.loops {
let mut coedges = Vec::new();
for usage in fragment_loop.coedges {
let source = source_edge(&usage.source, &index)?;
let dbg_desc = if dbg_asm {
Some(format!(
"{:?} bkey={:?} name={:?} s=({:.5},{:.5},{:.5}) e=({:.5},{:.5},{:.5}) face={}:{}",
std::mem::discriminant(&usage.source),
source.boundary_key,
source.name,
source.start.x, source.start.y, source.start.z,
source.end.x, source.end.y, source.end.z,
dbg_operand, dbg_face,
))
} else {
None
};
let (edge_id, reversed) = assembler.edge(source, usage.forward)?;
if let Some(desc) = dbg_desc {
dbg_edge_src.entry(edge_id).or_default().push(desc);
}
coedges.push(CoedgeRecord {
id: assembler.next_coedge_id,
edge_id,
forward: if reversed {
!usage.forward
} else {
usage.forward
},
pcurve: usage.pcurve,
});
assembler.next_coedge_id += 1;
}
loops.push(LoopRecord {
id: assembler.next_loop_id,
coedges,
});
assembler.next_loop_id += 1;
}
let name =
face_name_in_solids(&index, fragment.operand, fragment.source_face_id).map(|base| {
if used_face_names.insert(base.clone()) {
base
} else {
let mut suffix = 1usize;
loop {
let candidate = format!("{base}_{suffix}");
if used_face_names.insert(candidate.clone()) {
break candidate;
}
suffix += 1;
}
}
});
faces.push(FaceRecord {
id: assembler.next_face_id,
surface: fragment.surface,
same_sense: fragment.same_sense,
loops,
name,
});
assembler.next_face_id += 1;
}
if profile {
eprintln!(
"assemble.weld_ms={:.2} edges={} vertices={}",
stage_started.elapsed().as_secs_f64() * 1_000.0,
assembler.edges.len(),
assembler.vertices.len()
);
}
if dbg_asm {
let only_oneuse = std::env::var("BREP_DEBUG_ASM_EDGES").as_deref() != Ok("all");
for edge in &assembler.edges {
let uses = assembler.edge_use_counts.get(&edge.id).copied().unwrap_or(0);
if only_oneuse && uses > 1 {
continue;
}
eprintln!("ASM edge {} uses={} sources:", edge.id, uses);
for desc in dbg_edge_src.get(&edge.id).into_iter().flatten() {
eprintln!(" {desc}");
}
}
}
let stage_started = Instant::now();
conform_overlapping_one_use_edges(&mut assembler, &mut faces)?;
if !allow_open {
weld_identical_coincident_one_use_edges(&mut assembler, &mut faces)?;
}
if profile {
eprintln!(
"assemble.conform_ms={:.2}",
stage_started.elapsed().as_secs_f64() * 1_000.0
);
}
let stage_started = Instant::now();
let mut parent = (0..faces.len()).collect::<Vec<_>>();
fn find(parent: &mut [usize], mut index: usize) -> usize {
while parent[index] != index {
parent[index] = parent[parent[index]];
index = parent[index];
}
index
}
let mut edge_faces: HashMap<u64, Vec<usize>> = HashMap::default();
for (face_index, face) in faces.iter().enumerate() {
for coedge in face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
edge_faces
.entry(coedge.edge_id)
.or_default()
.push(face_index);
}
}
for connected in edge_faces.values() {
for pair in connected.windows(2) {
let a = find(&mut parent, pair[0]);
let b = find(&mut parent, pair[1]);
if a != b {
parent[a] = b;
}
}
}
let mut groups: HashMap<usize, Vec<FaceRecord>> = HashMap::default();
for (index, face) in faces.into_iter().enumerate() {
let root = find(&mut parent, index);
groups.entry(root).or_default().push(face);
}
let mut shells = groups
.into_values()
.enumerate()
.map(|(index, faces)| ShellRecord {
id: index as u64 + 1,
faces,
})
.collect::<Vec<_>>();
let used_edges = edge_faces.keys().copied().collect::<HashSet<_>>();
assembler.edges.retain(|edge| used_edges.contains(&edge.id));
let used_vertices = assembler
.edges
.iter()
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect::<HashSet<_>>();
assembler
.vertices
.retain(|vertex| used_vertices.contains(&vertex.id));
if profile {
eprintln!(
"assemble.shells_ms={:.2}",
stage_started.elapsed().as_secs_f64() * 1_000.0
);
}
if allow_open {
return Ok(BrepSolid {
id: 1,
vertices: assembler.vertices,
edges: assembler.edges,
shells,
genus: 0,
});
}
let face_count: i64 = shells.iter().map(|shell| shell.faces.len() as i64).sum();
let hole_count: i64 = shells
.iter()
.flat_map(|shell| &shell.faces)
.map(|face| face.loops.len().saturating_sub(1) as i64)
.sum();
let mut edge_count = assembler
.edges
.iter()
.filter(|edge| !edge.degenerate)
.count() as i64;
let euler = assembler.vertices.len() as i64 - edge_count + face_count - hole_count;
let mut numerator = shells.len() as i64 * 2 - euler;
if numerator < 0 || numerator % 2 != 0 {
let mut welded = 0usize;
while numerator < 0 || numerator % 2 != 0 {
let step = weld_tangency_duplicate_vertices(
&mut assembler.vertices,
&mut assembler.edges,
tolerance,
1,
);
if step == 0 {
break;
}
welded += step;
let edge_count = assembler
.edges
.iter()
.filter(|edge| !edge.degenerate)
.count() as i64;
let euler = assembler.vertices.len() as i64 - edge_count + face_count - hole_count;
numerator = shells.len() as i64 * 2 - euler;
}
if welded > 0 && std::env::var("BREP_DEBUG_BOOL").is_ok() {
eprintln!(
"boolean assembly welded {welded} tangency duplicate vertices (numerator now {numerator})"
);
}
}
if numerator < 0 || numerator % 2 != 0 {
let conformed = conform_unmatched_one_use_edges(
&mut assembler.vertices,
&mut assembler.edges,
&mut shells,
tolerance,
)?;
if conformed > 0 {
CONFORMANCE_REPAIRS.with(|count| count.set(count.get() + conformed as u64));
edge_count = assembler
.edges
.iter()
.filter(|edge| !edge.degenerate)
.count() as i64;
let euler = assembler.vertices.len() as i64 - edge_count + face_count - hole_count;
numerator = shells.len() as i64 * 2 - euler;
edge_faces.clear();
for shell in &shells {
for face in &shell.faces {
for coedge in face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
edge_faces
.entry(coedge.edge_id)
.or_default()
.push(face.id as usize - 1);
}
}
}
if std::env::var("BREP_DEBUG_BOOL").is_ok() {
eprintln!(
"boolean assembly conformed {conformed} unmatched one-use edges (numerator now {numerator})"
);
}
}
}
if numerator < 0 || numerator % 2 != 0 {
if !allow_open {
let provisional = BrepSolid {
id: 1,
vertices: assembler.vertices.clone(),
edges: assembler.edges.clone(),
shells: shells.clone(),
genus: 0,
};
if let Some(repaired) = repair_open_assembly_via_heal_chain(provisional, tolerance) {
if std::env::var("BREP_DEBUG_BOOL").is_ok() {
let one_use_before =
edge_faces.values().filter(|faces| faces.len() == 1).count();
eprintln!(
"boolean assembly repaired {one_use_before} one-use edges via heal chain"
);
}
return Ok(repaired);
}
}
let one_use = edge_faces
.iter()
.filter(|(_, faces)| faces.len() == 1)
.map(|(edge_id, _)| *edge_id)
.collect::<Vec<_>>();
let overused = edge_faces
.iter()
.filter(|(_, faces)| faces.len() > 2)
.map(|(edge_id, faces)| (*edge_id, faces.len()))
.collect::<Vec<_>>();
let mut one_use_by_face = HashMap::<usize, usize>::default();
for faces in edge_faces.values().filter(|faces| faces.len() == 1) {
*one_use_by_face.entry(faces[0] + 1).or_default() += 1;
}
let shell_faces = shells
.iter()
.map(|shell| shell.faces.iter().map(|face| face.id).collect::<Vec<_>>())
.collect::<Vec<_>>();
let one_use_geometry = one_use
.iter()
.filter_map(|edge_id| {
let edge = assembler.edges.iter().find(|edge| edge.id == *edge_id)?;
let start = assembler
.vertices
.iter()
.find(|vertex| vertex.id == edge.start_vertex_id)?
.point;
let end = assembler
.vertices
.iter()
.find(|vertex| vertex.id == edge.end_vertex_id)?
.point;
let middle = edge
.curve
.evaluate((edge.t0 + edge.t1) / 2.0)
.unwrap_or(start);
Some((
*edge_id,
edge.degenerate,
edge.curve.degree,
start,
middle,
end,
))
})
.take(40)
.collect::<Vec<_>>();
let mut geometric_matches = Vec::new();
for (index, first_id) in one_use.iter().enumerate() {
let first = assembler
.edges
.iter()
.find(|edge| edge.id == *first_id)
.unwrap();
for second_id in one_use.iter().skip(index + 1) {
let second = assembler
.edges
.iter()
.find(|edge| edge.id == *second_id)
.unwrap();
if edge_lies_on(first, second, 2e-3).unwrap_or(false)
|| edge_lies_on(second, first, 2e-3).unwrap_or(false)
{
geometric_matches.push((*first_id, *second_id));
}
}
}
return Err(KernelRefusal::new(
RefusalClass::NonIntegralGenus {
shells: shells.len() as u32,
euler: assembler.vertices.len() as i64 - edge_count as i64 + face_count as i64
- hole_count as i64,
},
KernelStage::Validate,
format!(
"boolean assembly: non-integral genus \
(V={} E={} F={} H={} S={} shell_faces={:?} \
one_use={} {:?} one_use_geometry={one_use_geometry:?} \
geometric_matches={geometric_matches:?} \
one_use_by_face={:?} overused={} {:?})",
assembler.vertices.len(),
edge_count,
face_count,
hole_count,
shells.len(),
shell_faces,
one_use.len(),
one_use.iter().take(24).collect::<Vec<_>>(),
one_use_by_face,
overused.len(),
overused.iter().take(24).collect::<Vec<_>>(),
)));
}
let solid = BrepSolid {
id: 1,
vertices: assembler.vertices,
edges: assembler.edges,
shells,
genus: numerator / 2,
};
let stage_started = Instant::now();
let mut solid = merge_curve_continuation_edges(&solid, tolerance).or_refuse(KernelStage::Sew, "merge_curve_continuation_edges")?;
if profile {
eprintln!(
"assemble.continuation_ms={:.2}",
stage_started.elapsed().as_secs_f64() * 1_000.0
);
}
let stage_started = Instant::now();
let polished = polish_triple_junction_vertices(&mut solid, tolerance)?;
if profile {
eprintln!(
"assemble.junction_ms={:.2} polished={polished}",
stage_started.elapsed().as_secs_f64() * 1_000.0
);
}
refresh_derived_genus(&mut solid);
let stage_started = Instant::now();
let mut issues = solid.validate();
if profile {
eprintln!(
"assemble.validate_ms={:.2}",
stage_started.elapsed().as_secs_f64() * 1_000.0
);
}
if !issues.is_empty() {
let conformed = conform_unmatched_one_use_edges(
&mut solid.vertices,
&mut solid.edges,
&mut solid.shells,
tolerance,
)?;
if conformed > 0 {
CONFORMANCE_REPAIRS.with(|count| count.set(count.get() + conformed as u64));
refresh_derived_genus(&mut solid);
issues = solid.validate();
if std::env::var("BREP_DEBUG_BOOL").is_ok() {
eprintln!(
"boolean assembly conformed {conformed} unmatched one-use edges at validation ({} issues remain)",
issues.len()
);
}
}
}
if !issues.is_empty()
&& issues.iter().all(|issue| {
issue.severity == "error"
&& matches!(issue.kind, crate::IssueKind::CurveVertexGap { .. })
})
{
let gap_band = commit_weld(tolerance) * 10.0;
let vertex_points = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect::<HashMap<_, _>>();
let mut snapped_any = false;
for edge in &mut solid.edges {
if edge.degenerate {
continue;
}
let (Some(&start), Some(&end)) = (
vertex_points.get(&edge.start_vertex_id),
vertex_points.get(&edge.end_vertex_id),
) else {
continue;
};
let start_gap = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Sew, "evaluate")?.sub(start).length();
let end_gap = edge.curve.evaluate(edge.t1).or_refuse(KernelStage::Sew, "evaluate")?.sub(end).length();
if start_gap <= 1e-9 && end_gap <= 1e-9 {
continue;
}
if start_gap > gap_band || end_gap > gap_band {
continue;
}
let curve = snap_edge_curve_endpoints(edge.curve.clone(), edge.t0, edge.t1, start, end)?;
let [t0, t1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
edge.curve = curve;
edge.t0 = t0;
edge.t1 = t1;
snapped_any = true;
}
if snapped_any {
refresh_derived_genus(&mut solid);
issues = solid.validate();
if std::env::var("BREP_DEBUG_BOOL").is_ok() {
eprintln!(
"boolean assembly snapped curve endpoints onto vertices ({} issues remain)",
issues.len()
);
}
}
}
if !issues.is_empty() {
let mut use_counts = HashMap::<u64, usize>::default();
for coedge in solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
{
*use_counts.entry(coedge.edge_id).or_default() += 1;
}
let open_edges = solid
.edges
.iter()
.filter(|edge| use_counts.get(&edge.id).copied().unwrap_or(0) == 1)
.map(|edge| {
let start = solid
.vertices
.iter()
.find(|vertex| vertex.id == edge.start_vertex_id)
.map(|vertex| vertex.point);
let end = solid
.vertices
.iter()
.find(|vertex| vertex.id == edge.end_vertex_id)
.map(|vertex| vertex.point);
format!("{}:{start:?}->{end:?}", edge.id)
})
.collect::<Vec<_>>();
if std::env::var("BREP_DEBUG_VALIDATE").is_ok() {
for issue in &issues {
if let Some(rest) = issue.message.strip_prefix("coedge ") {
if let Some(edge_part) = rest.split("edge ").nth(1) {
if let Some(edge_id) = edge_part
.split_whitespace()
.next()
.and_then(|token| token.parse::<u64>().ok())
{
if let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) {
let [d0, d1] = edge.curve.domain().unwrap_or([0.0, 0.0]);
eprintln!(
"VALIDATE edge {}: name={:?} degen={} t=[{:.6},{:.6}] domain=[{:.6},{:.6}] cps={}",
edge.id,
edge.name,
edge.degenerate,
edge.t0,
edge.t1,
d0,
d1,
edge.curve.control_points.len()
);
for fraction in [0.0f64, 0.5, 1.0] {
if let Ok(point) = edge
.curve
.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
{
eprintln!(
" edge point {:.2}: ({:.6},{:.6},{:.6})",
fraction, point.x, point.y, point.z
);
}
}
}
}
}
}
}
}
return Err(KernelRefusal::new(
RefusalClass::DegenerateArrangement {
open_edges: open_edges.len() as u32,
issues: issues.len() as u32,
},
KernelStage::Validate,
format!(
"boolean assembly produced invalid topology (S={} genus={}): {issues:?}; open edges [{}]",
solid.shells.len(),
solid.genus,
open_edges.join(", ")
)));
}
let stage_started = Instant::now();
let volume = solid_signed_volume(&solid).or_refuse(KernelStage::Sew, "solid_signed_volume")?;
if profile {
eprintln!(
"assemble.mass_check_ms={:.2}",
stage_started.elapsed().as_secs_f64() * 1_000.0
);
}
if volume <= tolerance.powi(3) {
return Err(KernelRefusal::new(
RefusalClass::NonPositiveVolume,
KernelStage::Validate,
"boolean assembly produced non-positive volume",
));
}
Ok(solid)
}
pub(crate) fn assemble_fragments(
selected: Vec<FaceFragmentRecord>,
solids: &HashMap<u8, &BrepSolid>,
imprint: &ImprintResultRecord,
tolerance: f64,
) -> Result<BrepSolid, KernelRefusal> {
assemble_fragments_impl(selected, solids, imprint, tolerance, false)
}
pub(crate) fn assemble_open_fragments(
selected: Vec<FaceFragmentRecord>,
solids: &HashMap<u8, &BrepSolid>,
imprint: &ImprintResultRecord,
tolerance: f64,
) -> Result<BrepSolid, KernelRefusal> {
assemble_fragments_impl(selected, solids, imprint, tolerance, true)
}