use crate::{KernelRefusal, KernelStage, OrRefuse, SoundnessDefect};
use crate::topology::{BrepSolid, LoopRecord};
use crate::{solid_self_intersections, SelfIntersectionOptions, SelfIntersectionReport};
pub fn accept_sound(solid: BrepSolid, entry: &str) -> Result<BrepSolid, KernelRefusal> {
let report = scan(&solid, entry)?;
if !report.is_flagged() {
return accept_or_break_out(solid, entry);
}
dump_flagged(&solid, entry);
if let Some(refusal) = fold_refusal(&report, entry) {
return Err(refusal);
}
let repaired = match resolve_face_crossing(&solid) {
Ok(repaired) => repaired,
Err(reason) if first_hole_breach(&solid).is_some() => {
match break_out_until_contained(solid.clone(), entry) {
Ok(repaired) => repaired,
Err(second) => {
return Err(crossing_refusal(
&report,
entry,
&format!(
"{reason}. The breakout route was offered it too and declined: \
{second}"
),
))
}
}
}
Err(reason) => return Err(crossing_refusal(&report, entry, &reason.message)),
};
let after = scan(&repaired, entry)?;
if after.is_flagged() {
return Err(crossing_refusal(
&report,
entry,
&format!(
"the repair ran and the result still self-intersects — {}",
after.summary().unwrap_or_else(|| "no summary".into())
),
));
}
accept_or_break_out(repaired, entry)
}
const MAX_BREAKOUT_ROUNDS: usize = 4;
fn accept_or_break_out(solid: BrepSolid, entry: &str) -> Result<BrepSolid, KernelRefusal> {
if refuse_holes_leaving_their_faces(&solid, entry).is_ok() {
return Ok(solid);
}
break_out_until_contained(solid, entry)
}
fn break_out_until_contained(solid: BrepSolid, entry: &str) -> Result<BrepSolid, KernelRefusal> {
let mut current = solid;
for _ in 0..MAX_BREAKOUT_ROUNDS {
let Some(breach) = first_hole_breach(¤t) else {
let report = scan(¤t, entry)?;
if report.is_flagged() {
return Err(KernelRefusal::unsound(
KernelStage::Validate,
SoundnessDefect::Crossing,
crossing_faces(&report),
format!(
"{entry}: the breakout repair built a body and it SELF-INTERSECTS — {}",
report.summary().unwrap_or_else(|| "no summary".into()),
),
));
}
return Ok(current);
};
let floor = hole_breach_refusal(¤t, &breach, entry);
current = repair_hole_breakout(¤t, &breach).map_err(|reason| {
floor.with_message(|floor| {
format!("{floor}. The breakout repair was offered this body and declined it: {reason}")
})
})?;
}
let breach = first_hole_breach(¤t).expect("the loop only falls through on a breach");
Err(
hole_breach_refusal(¤t, &breach, entry).with_message(|floor| {
format!(
"{floor}. {MAX_BREAKOUT_ROUNDS} breakouts were built and a hole is still off its face, so \
this stops rather than repairing a body into one nobody asked for"
)
}),
)
}
pub fn accept_sound_against(
inputs: &[&BrepSolid],
solid: BrepSolid,
entry: &str,
) -> Result<BrepSolid, KernelRefusal> {
let report = scan(&solid, entry)?;
if !report.is_flagged() {
return Ok(solid);
}
for input in inputs {
let before = scan(input, entry)?;
if before.is_flagged() {
return Ok(solid);
}
}
accept_sound(solid, entry)
}
fn scan(solid: &BrepSolid, entry: &str) -> Result<SelfIntersectionReport, KernelRefusal> {
let options = SelfIntersectionOptions::for_solid(solid);
solid_self_intersections(solid, options).map_err(|message| {
KernelRefusal::internal(
KernelStage::Validate,
"soundness_scan",
format!("{entry}: the soundness scan could not run: {message}"),
)
})
}
fn crossing_faces(report: &SelfIntersectionReport) -> Vec<u64> {
let mut faces = Vec::new();
for crossing in &report.confirmed {
for face in [crossing.face_a, crossing.face_b] {
if !faces.contains(&face) {
faces.push(face);
}
}
}
faces
}
fn fold_refusal(report: &SelfIntersectionReport, entry: &str) -> Option<KernelRefusal> {
let fold = report.folds.first()?;
let faces = report.folds.iter().map(|fold| fold.face).collect();
let message = format!(
"{entry}: the result's face {}{} FOLDS THROUGH ITSELF in 3D — its surface carries \
({:.6}, {:.6}) and ({:.6}, {:.6}), {:.3e} apart in its own domain and both inside \
its trim, to the same point ({:.4}, {:.4}, {:.4}) (residual {:.3e}, branches {:.1} \
degrees apart){}. The face is wrong in AREA where its volume is not, so nothing that \
measures volume notices and `validate()` cannot see it at all. A fold is a trim that \
was never constructed: the folded part of the parameter region has to be carved away \
where the carrier is built, not repaired out of the finished body.",
fold.face,
fold.face_name
.as_ref()
.map(|name| format!(" '{name}'"))
.unwrap_or_default(),
fold.uv_a[0],
fold.uv_a[1],
fold.uv_b[0],
fold.uv_b[1],
fold.uv_separation,
fold.point.x,
fold.point.y,
fold.point.z,
fold.residual,
fold.normal_angle.to_degrees(),
if report.folds.len() > 1 {
format!(" ({} folded faces in all)", report.folds.len())
} else {
String::new()
},
);
Some(KernelRefusal::unsound(
KernelStage::Validate,
SoundnessDefect::Fold,
faces,
message,
))
}
fn crossing_refusal(report: &SelfIntersectionReport, entry: &str, reason: &str) -> KernelRefusal {
let pairs = report
.confirmed
.iter()
.map(|crossing| {
format!(
"faces {} and {} near ({:.4}, {:.4}, {:.4}) (straddle {:.3e} against a band of {:.1e})",
crossing.face_a,
crossing.face_b,
crossing.point.x,
crossing.point.y,
crossing.point.z,
crossing.straddle,
report.options.straddle_band
)
})
.collect::<Vec<_>>()
.join("; ");
KernelRefusal::unsound(
KernelStage::Validate,
SoundnessDefect::Crossing,
crossing_faces(report),
format!(
"{entry}: the result SELF-INTERSECTS — {pairs}. The enclosed space can still measure \
right, so neither a volume oracle nor `validate()` would have caught this. The repair \
that splits both faces along their carriers' intersection and drops the enclosed \
material did not take it: {reason}"
),
)
}
const LOOP_SAMPLES_PER_SPAN: usize = 32;
fn loop_parameter_polyline(loop_record: &LoopRecord) -> Result<Vec<(f64, f64)>, KernelRefusal> {
pcurve_polyline(loop_record.coedges.iter().map(|coedge| &coedge.pcurve))
}
fn fragment_loop_polyline(
fragment_loop: &crate::fragment::FragmentLoop,
) -> Result<Vec<(f64, f64)>, KernelRefusal> {
pcurve_polyline(fragment_loop.coedges.iter().map(|coedge| &coedge.pcurve))
}
fn pcurve_polyline<'a>(
pcurves: impl Iterator<Item = &'a crate::NurbsCurve>,
) -> Result<Vec<(f64, f64)>, KernelRefusal> {
let mut points = Vec::new();
for pcurve in pcurves {
let [d0, d1] = pcurve.domain().or_refuse(KernelStage::Validate, "domain")?;
let steps = if pcurve.degree <= 1 && pcurve.control_points.len() == 2 {
1
} else {
let mut spans = 0usize;
for window in pcurve.knots.windows(2) {
if window[1] > window[0] {
spans += 1;
}
}
spans.max(1) * LOOP_SAMPLES_PER_SPAN
};
for step in 0..steps {
let uv = pcurve.evaluate(d0 + (d1 - d0) * (step as f64 / steps as f64)).or_refuse(KernelStage::Validate, "evaluate")?;
points.push((uv.x, uv.y));
}
}
Ok(points)
}
fn polyline_area(points: &[(f64, f64)]) -> f64 {
let mut twice = 0.0;
for index in 0..points.len() {
let (x0, y0) = points[index];
let (x1, y1) = points[(index + 1) % points.len()];
twice += x0 * y1 - x1 * y0;
}
0.5 * twice
}
struct BoundaryBands<'a> {
points: &'a [(f64, f64)],
v_lo: f64,
bands_per_v: f64,
starts: Vec<u32>,
segments: Vec<u32>,
}
impl<'a> BoundaryBands<'a> {
fn build(points: &'a [(f64, f64)]) -> Self {
let count = points.len();
debug_assert!(count < u32::MAX as usize, "segment indices are u32");
let (mut v_lo, mut v_hi) = (f64::INFINITY, f64::NEG_INFINITY);
let mut variation = 0.0;
for index in 0..count {
let (_, y0) = points[index];
let (_, y1) = points[(index + 1) % count];
v_lo = v_lo.min(y0);
v_hi = v_hi.max(y0);
variation += (y1 - y0).abs();
}
let span = v_hi - v_lo;
let bands = if span.is_finite() && span > 0.0 && variation > 0.0 {
let wanted = span * count as f64 / variation;
if wanted.is_finite() && wanted >= 1.0 {
(wanted as usize).min(count.max(1))
} else {
1
}
} else {
1
};
let bands_per_v = if bands > 1 { bands as f64 / span } else { 0.0 };
let band_of = |v: f64| -> usize {
if bands_per_v == 0.0 {
return 0;
}
let raw = (v - v_lo) * bands_per_v;
if !(raw > 0.0) {
0
} else if raw >= (bands - 1) as f64 {
bands - 1
} else {
raw as usize
}
};
let mut starts = vec![0u32; bands + 1];
for index in 0..count {
let (_, y0) = points[index];
let (_, y1) = points[(index + 1) % count];
let (lo, hi) = (band_of(y0.min(y1)), band_of(y0.max(y1)));
for start in starts[lo + 1..=hi + 1].iter_mut() {
*start += 1;
}
}
for band in 1..=bands {
starts[band] += starts[band - 1];
}
let mut cursor = starts.clone();
let mut segments = vec![0u32; starts[bands] as usize];
for index in 0..count {
let (_, y0) = points[index];
let (_, y1) = points[(index + 1) % count];
let (lo, hi) = (band_of(y0.min(y1)), band_of(y0.max(y1)));
for band in lo..=hi {
segments[cursor[band] as usize] = index as u32;
cursor[band] += 1;
}
}
Self {
points,
v_lo,
bands_per_v,
starts,
segments,
}
}
fn band_count(&self) -> usize {
self.starts.len() - 1
}
fn band_of(&self, v: f64) -> usize {
if self.bands_per_v == 0.0 {
return 0;
}
let bands = self.band_count();
let raw = (v - self.v_lo) * self.bands_per_v;
if !(raw > 0.0) {
0
} else if raw >= (bands - 1) as f64 {
bands - 1
} else {
raw as usize
}
}
fn segment(&self, index: u32) -> ((f64, f64), (f64, f64)) {
let index = index as usize;
(
self.points[index],
self.points[(index + 1) % self.points.len()],
)
}
fn contains(&self, point: (f64, f64)) -> bool {
let (u, v) = point;
let band = self.band_of(v);
let mut inside = false;
for &index in &self.segments[self.starts[band] as usize..self.starts[band + 1] as usize] {
let ((x0, y0), (x1, y1)) = self.segment(index);
if (y0 > v) != (y1 > v) && u < x0 + (v - y0) * (x1 - x0) / (y1 - y0) {
inside = !inside;
}
}
inside
}
fn within(&self, point: (f64, f64), tolerance: f64) -> bool {
let (u, v) = point;
let lo = self.band_of(v - tolerance).saturating_sub(1);
let hi = (self.band_of(v + tolerance) + 1).min(self.band_count() - 1);
for band in lo..=hi {
for &index in &self.segments[self.starts[band] as usize..self.starts[band + 1] as usize]
{
let ((x0, y0), (x1, y1)) = self.segment(index);
let (dx, dy) = (x1 - x0, y1 - y0);
let length_squared = dx * dx + dy * dy;
let t = if length_squared > 0.0 {
(((u - x0) * dx + (v - y0) * dy) / length_squared).clamp(0.0, 1.0)
} else {
0.0
};
let (px, py) = (x0 + t * dx, y0 + t * dy);
if ((u - px).powi(2) + (v - py).powi(2)).sqrt() <= tolerance {
return true;
}
}
}
false
}
fn nearest(&self, point: (f64, f64)) -> ((f64, f64), f64) {
let (u, v) = point;
let bands = self.band_count();
let centre = self.band_of(v);
let band_height = if self.bands_per_v > 0.0 { 1.0 / self.bands_per_v } else { f64::INFINITY };
let mut best = f64::INFINITY;
let mut best_point = self.points[0];
let search = |band: usize, best: &mut f64, best_point: &mut (f64, f64)| {
for &index in &self.segments[self.starts[band] as usize..self.starts[band + 1] as usize] {
let ((x0, y0), (x1, y1)) = self.segment(index);
let (dx, dy) = (x1 - x0, y1 - y0);
let length_squared = dx * dx + dy * dy;
let t = if length_squared > 0.0 {
(((u - x0) * dx + (v - y0) * dy) / length_squared).clamp(0.0, 1.0)
} else {
0.0
};
let candidate = (x0 + t * dx, y0 + t * dy);
let distance = ((u - candidate.0).powi(2) + (v - candidate.1).powi(2)).sqrt();
if distance < *best {
*best = distance;
*best_point = candidate;
}
}
};
for ring in 0..bands {
if ring > 1 && (ring - 1) as f64 * band_height > best {
break;
}
let below = centre.checked_sub(ring);
let above = if centre + ring < bands { Some(centre + ring) } else { None };
if below.is_none() && above.is_none() {
break;
}
if let Some(band) = below {
search(band, &mut best, &mut best_point);
}
if let Some(band) = above {
if ring > 0 {
search(band, &mut best, &mut best_point);
}
}
}
(best_point, best)
}
}
fn refuse_holes_leaving_their_faces(solid: &BrepSolid, entry: &str) -> Result<(), KernelRefusal> {
match first_hole_breach(solid) {
Some(breach) => Err(hole_breach_refusal(solid, &breach, entry)),
None => Ok(()),
}
}
struct HoleBreach {
face: u64,
loop_index: usize,
loop_id: u64,
at: (f64, f64),
banded: bool,
}
fn first_hole_breach(solid: &BrepSolid) -> Option<HoleBreach> {
for shell in &solid.shells {
for face in &shell.faces {
if face.loops.len() < 2 {
continue;
}
let fitted = !face.surface.is_affine().unwrap_or(false);
let closed = face.surface.closed_directions().unwrap_or((false, false));
let period = (
face.surface.domain_u().map(|[a, b]| b - a).unwrap_or(0.0),
face.surface.domain_v().map(|[a, b]| b - a).unwrap_or(0.0),
);
let windings: Vec<(i32, i32)> = face
.loops
.iter()
.map(|record| loop_winding(record, closed, period))
.collect();
let polylines = face
.loops
.iter()
.map(loop_parameter_polyline)
.collect::<Result<Vec<_>, KernelRefusal>>()
.unwrap_or_default();
if polylines.len() != face.loops.len() || polylines.iter().any(Vec::is_empty) {
continue;
}
let breach = if windings.iter().all(|&winding| winding == (0, 0)) {
let spherical = matches!(face.surface.analytic(), Some(crate::AnalyticSurface::Sphere { .. }));
if spherical && !sphere_has_parameter_outer(solid, face, &polylines) {
spherical_hole_breach(solid, face)
} else {
nested_hole_breach(face, &polylines, fitted, closed, period)
}
} else {
banded_hole_breach(face, &polylines, &windings, fitted, closed, period)
};
if breach.is_some() {
return breach;
}
}
}
None
}
fn sphere_has_parameter_outer(
solid: &BrepSolid,
face: &crate::FaceRecord,
polylines: &[Vec<(f64, f64)>],
) -> bool {
let areas: Vec<_> = polylines.iter().map(|points| polyline_area(points)).collect();
let Some(outer) = (0..areas.len()).max_by(|&a, &b| areas[a].abs().total_cmp(&areas[b].abs())) else {
return false;
};
let orientation = if face.same_sense { 1.0 } else { -1.0 };
if areas[outer] * orientation <= 0.0 { return false; }
let real = |index: usize| face.loops[index].coedges.iter().any(|coedge| {
face.loops.iter().flat_map(|rim| &rim.coedges)
.filter(|other| other.edge_id == coedge.edge_id).count() == 1
&& !solid.edges.iter().any(|edge| edge.id == coedge.edge_id && edge.degenerate)
});
real(outer) && (0..areas.len()).all(|i| i == outer || !real(i) || areas[i] * orientation <= 0.0)
}
fn spherical_hole_breach(solid: &BrepSolid, face: &crate::FaceRecord) -> Option<HoleBreach> {
let mut uses = std::collections::HashMap::new();
for coedge in face.loops.iter().flat_map(|rim| &rim.coedges) {
*uses.entry(coedge.edge_id).or_insert(0usize) += 1;
}
for (index, rim) in face.loops.iter().enumerate() {
let mut other = face.clone();
other.loops.remove(index);
other.loops.retain(|boundary| {
boundary.coedges.iter().any(|coedge| {
uses[&coedge.edge_id] == 1
&& !solid.edges.iter().any(|edge| {
edge.id == coedge.edge_id && edge.degenerate
})
})
});
if other.loops.is_empty() {
continue;
}
let [u0, u1] = face.surface.domain_u().ok()?;
for coedge in &rim.coedges {
if uses[&coedge.edge_id] > 1
|| solid
.edges
.iter()
.any(|edge| edge.id == coedge.edge_id && edge.degenerate)
{
continue;
}
let boundary = LoopRecord {
id: rim.id,
coedges: vec![coedge.clone()],
};
let Ok(mut points) = loop_parameter_polyline(&boundary) else {
continue;
};
if coedge.pcurve.degree == 1 && coedge.pcurve.control_points.len() == 2 {
let [a, b] = coedge.pcurve.domain().ok()?;
let midpoint = coedge.pcurve.evaluate((a + b) * 0.5).ok()?;
points.push((midpoint.x, midpoint.y));
}
for (u, v) in points {
if crate::parameter_point_in_face(
&other,
crate::Vec2 {
x: u0 + (u - u0).rem_euclid(u1 - u0),
y: v,
},
1e-6,
)
.ok()
!= Some(crate::PolygonClass::Outside)
{
continue;
}
return Some(HoleBreach {
face: face.id,
loop_index: index,
loop_id: rim.id,
at: (u, v),
banded: true,
});
}
}
}
None
}
fn loop_winding(loop_record: &LoopRecord, closed: (bool, bool), period: (f64, f64)) -> (i32, i32) {
let (mut du, mut dv) = (0.0, 0.0);
for coedge in &loop_record.coedges {
let Ok([d0, d1]) = coedge.pcurve.domain() else { return (0, 0) };
let (Ok(start), Ok(end)) = (coedge.pcurve.evaluate(d0), coedge.pcurve.evaluate(d1)) else {
return (0, 0);
};
du += end.x - start.x;
dv += end.y - start.y;
}
let wind = |closed: bool, net: f64, period: f64| {
if closed && period > 0.0 && net.is_finite() {
(net / period).round() as i32
} else {
0
}
};
(wind(closed.0, du, period.0), wind(closed.1, dv, period.1))
}
fn has_seam_pair(loop_record: &LoopRecord) -> bool {
loop_record.coedges.iter().enumerate().any(|(index, coedge)| {
loop_record.coedges[index + 1..]
.iter()
.any(|other| other.edge_id == coedge.edge_id)
})
}
fn nested_hole_breach(
face: &crate::FaceRecord,
polylines: &[Vec<(f64, f64)>],
fitted: bool,
closed: (bool, bool),
period: (f64, f64),
) -> Option<HoleBreach> {
let outer_index = (0..polylines.len())
.max_by(|&a, &b| {
polyline_area(&polylines[a])
.abs()
.total_cmp(&polyline_area(&polylines[b]).abs())
})
.unwrap_or(0);
let outer = &polylines[outer_index];
let bands = BoundaryBands::build(outer);
let tolerance = boundary_tolerance(outer);
let seamed = has_seam_pair(&face.loops[outer_index]);
let (mut lo, mut hi) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));
for &(u, v) in outer {
lo = (lo.0.min(u), lo.1.min(v));
hi = (hi.0.max(u), hi.1.max(v));
}
let centre = (0.5 * (lo.0 + hi.0), 0.5 * (lo.1 + hi.1));
let fold_u = closed.0 && !seamed && period.0 > 0.0;
let fold_v = closed.1 && !seamed && period.1 > 0.0;
let fold = |(u, v): (f64, f64)| {
(
if fold_u { u + period.0 * ((centre.0 - u) / period.0).round() } else { u },
if fold_v { v + period.1 * ((centre.1 - v) / period.1).round() } else { v },
)
};
for (index, hole) in polylines.iter().enumerate() {
if index == outer_index {
continue;
}
let Some(at) = hole.iter().map(|&point| fold(point)).find(|&point| {
!bands.contains(point)
&& !bands.within(point, tolerance)
&& !(fitted && acquitted_in_space(&face.surface, point, bands.nearest(point).0))
}) else {
continue;
};
return Some(HoleBreach {
face: face.id,
loop_index: index,
loop_id: face.loops[index].id,
at,
banded: false,
});
}
None
}
struct Ring {
segments: Vec<((f64, f64), (f64, f64))>,
material_positive: bool,
positive_winding: bool,
}
fn banded_hole_breach(
face: &crate::FaceRecord,
polylines: &[Vec<(f64, f64)>],
windings: &[(i32, i32)],
fitted: bool,
closed: (bool, bool),
period: (f64, f64),
) -> Option<HoleBreach> {
let rings_wind = |axis: usize| {
windings
.iter()
.filter(|&&winding| winding != (0, 0))
.all(|&(u, v)| {
if axis == 0 {
u.abs() == 1 && v == 0
} else {
v.abs() == 1 && u == 0
}
})
};
let along = if rings_wind(0) && closed.0 && period.0 > 0.0 {
0
} else if rings_wind(1) && closed.1 && period.1 > 0.0 {
1
} else {
return None;
};
let open = 1 - along;
let axis = |point: (f64, f64), which: usize| if which == 0 { point.0 } else { point.1 };
let period_along = axis(period, along);
let transverse_period = if if open == 0 { closed.0 } else { closed.1 } {
let p = axis(period, open);
if p <= 0.0
|| windings
.iter()
.map(|&w| if along == 0 { w.0 } else { w.1 })
.sum::<i32>()
!= 0
{
return None;
}
Some(p)
} else {
None
};
let domain_start = if along == 0 {
face.surface.domain_u().ok()?[0]
} else {
face.surface.domain_v().ok()?[0]
};
let left = polylines
.iter()
.zip(windings)
.filter(|(_, &winding)| winding == (0, 0))
.map(|(hole, _)| {
let origin = hole[0];
let local: Vec<_> = hole.iter()
.map(|&(u, v)| (u - origin.0, v - origin.1))
.collect();
polyline_area(&local)
})
.find(|area| *area != 0.0)?
< 0.0;
let mut rings: Vec<Ring> = Vec::new();
let mut extent = 0.0f64;
for (index, record) in face.loops.iter().enumerate() {
let winding = windings[index];
if winding == (0, 0) {
continue;
}
let mut points: Vec<(f64, f64)> = Vec::new();
for coedge in &record.coedges {
points.extend(coedge_parameter_polyline(coedge).ok()?);
}
let first = *points.first()?;
points.push((
first.0 + winding.0 as f64 * period.0,
first.1 + winding.1 as f64 * period.1,
));
for window in points.windows(2) {
extent = extent
.max(axis(window[0], open) - axis(window[1], open))
.max(axis(window[1], open) - axis(window[0], open));
}
let segments = points
.windows(2)
.map(|window| (window[0], window[1]))
.collect();
let positive_winding = axis((winding.0 as f64, winding.1 as f64), along) > 0.0;
let material_positive = if along == 0 {
positive_winding == left
} else {
positive_winding != left
};
rings.push(Ring {
segments,
material_positive,
positive_winding,
});
}
let tolerance = (extent.max(period_along) * 1e-9).max(1e-12);
for (index, hole) in polylines.iter().enumerate() {
if windings[index] != (0, 0) {
continue;
}
for &station in hole {
let station = {
let s = axis(station, along);
let reduced = domain_start + (s - domain_start).rem_euclid(period_along);
if along == 0 {
(reduced, station.1)
} else {
(station.0, reduced)
}
};
let held = if let Some(transverse) = transverse_period {
periodic_band_holds(&rings, station, along, period_along, transverse)?
} else {
rings
.iter()
.all(|ring| ring_holds(ring, station, along, period_along))
};
if held {
continue;
}
let (nearest, distance) =
rings_nearest(&rings, station, along, period_along, transverse_period);
if distance <= tolerance {
continue;
}
if fitted && acquitted_in_space(&face.surface, station, nearest) {
continue;
}
return Some(HoleBreach {
face: face.id,
loop_index: index,
loop_id: face.loops[index].id,
at: station,
banded: true,
});
}
}
None
}
const RING_IMAGES: [i32; 5] = [-2, -1, 0, 1, 2];
fn ring_holds(ring: &Ring, station: (f64, f64), along: usize, period: f64) -> bool {
let (s_along, s_open) = if along == 0 {
(station.0, station.1)
} else {
(station.1, station.0)
};
let mut crossings = 0usize;
for &(a, b) in &ring.segments {
let (a_along, a_open) = if along == 0 { (a.0, a.1) } else { (a.1, a.0) };
let (b_along, b_open) = if along == 0 { (b.0, b.1) } else { (b.1, b.0) };
for image in RING_IMAGES {
let shift =
(((s_along - 0.5 * (a_along + b_along)) / period).round() + image as f64) * period;
let (a_s, b_s) = (a_along + shift, b_along + shift);
if (a_s > s_along) != (b_s > s_along) {
let at = a_open + (s_along - a_s) * (b_open - a_open) / (b_s - a_s);
if at > s_open {
crossings += 1;
}
}
}
}
let ring_is_positive = crossings % 2 == 1;
ring_is_positive != ring.material_positive
}
fn periodic_band_holds(
rings: &[Ring],
station: (f64, f64),
along: usize,
period: f64,
transverse: f64,
) -> Option<bool> {
let (s, t) = if along == 0 {
station
} else {
(station.1, station.0)
};
let mut events = Vec::new();
for ring in rings {
for &(a, b) in &ring.segments {
let (a_s, a_t) = if along == 0 { a } else { (a.1, a.0) };
let (b_s, b_t) = if along == 0 { b } else { (b.1, b.0) };
let centre = ((s - 0.5 * (a_s + b_s)) / period).round();
for image in RING_IMAGES {
let shift = (centre + image as f64) * period;
let (a_image, b_image) = (a_s + shift, b_s + shift);
if (a_image > s) != (b_image > s) {
let at = if s == a_image {
a_t
} else if s == b_image {
b_t
} else {
a_t + (s - a_s - shift) * (b_t - a_t) / (b_s - a_s)
};
let positive = if (b_s > a_s) == ring.positive_winding {
ring.material_positive
} else {
!ring.material_positive
};
events.push((
(at - t).rem_euclid(transverse),
if positive { 1i32 } else { -1 },
));
}
}
}
}
events.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut i = 0;
while i < events.len() {
let distance = events[i].0;
let mut sign = 0;
while i < events.len() && events[i].0 == distance {
sign += events[i].1;
i += 1;
}
if sign != 0 {
return Some(sign < 0);
}
}
None
}
fn rings_nearest(
rings: &[Ring],
station: (f64, f64),
along: usize,
period: f64,
transverse: Option<f64>,
) -> ((f64, f64), f64) {
let (u, v) = station;
let mut best = (station, f64::INFINITY);
for ring in rings {
for &(a, b) in &ring.segments {
for image in RING_IMAGES {
let midpoint = (0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
let (s, m) = if along == 0 {
(u, midpoint.0)
} else {
(v, midpoint.1)
};
let along_shift = (((s - m) / period).round() + image as f64) * period;
for across_image in if transverse.is_some() {
&RING_IMAGES[..]
} else {
&RING_IMAGES[2..3]
} {
let across_shift = transverse
.map(|p| {
let (s, m) = if along == 0 {
(v, midpoint.1)
} else {
(u, midpoint.0)
};
(((s - m) / p).round() + *across_image as f64) * p
})
.unwrap_or(0.0);
let shift = if along == 0 {
(along_shift, across_shift)
} else {
(across_shift, along_shift)
};
let (x0, y0) = (a.0 + shift.0, a.1 + shift.1);
let (x1, y1) = (b.0 + shift.0, b.1 + shift.1);
let (dx, dy) = (x1 - x0, y1 - y0);
let length_squared = dx * dx + dy * dy;
let t = if length_squared > 0.0 {
(((u - x0) * dx + (v - y0) * dy) / length_squared).clamp(0.0, 1.0)
} else {
0.0
};
let candidate = (x0 + t * dx, y0 + t * dy);
let distance = ((u - candidate.0).powi(2) + (v - candidate.1).powi(2)).sqrt();
if distance < best.1 {
best = (candidate, distance);
}
}
}
}
}
best
}
fn boundary_tolerance(outer: &[(f64, f64)]) -> f64 {
let (mut lo, mut hi) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));
for &(u, v) in outer {
lo = (lo.0.min(u), lo.1.min(v));
hi = (hi.0.max(u), hi.1.max(v));
}
let extent = (hi.0 - lo.0).max(hi.1 - lo.1);
(extent * 1e-9).max(1e-12)
}
const CURVED_BREACH_ACQUITTAL: f64 = 10.0 * crate::PCURVE_REFINEMENT_TOLERANCE;
fn acquitted_in_space(
surface: &crate::NurbsSurface,
point: (f64, f64),
nearest: (f64, f64),
) -> bool {
match (
surface.evaluate_extended(point.0, point.1),
surface.evaluate_extended(nearest.0, nearest.1),
) {
(Ok(here), Ok(there)) => here.sub(there).length() <= CURVED_BREACH_ACQUITTAL,
_ => false,
}
}
fn hole_breach_refusal(solid: &BrepSolid, breach: &HoleBreach, entry: &str) -> KernelRefusal {
let face = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == breach.face);
let label = match face.and_then(|face| face.name.as_deref()) {
Some(name) => format!("`{name}` (face {})", breach.face),
None => format!("face {}", breach.face),
};
let (u, v) = breach.at;
let where_in_space = face
.and_then(|face| face.surface.evaluate_extended(u, v).ok())
.map(|point| format!(" — ({:.4}, {:.4}, {:.4}) in space", point.x, point.y, point.z))
.unwrap_or_default();
KernelRefusal::unsound(
KernelStage::Validate,
SoundnessDefect::HoleBreach,
vec![breach.face],
format!(
"{entry}: hole loop {} of {label} has LEFT that face — it runs outside the \
face's own outer boundary, at ({u:.6}, {v:.6}) in the face's parameters{}. \
A hole is a hole IN a face; this one bounds a wall that no longer opens \
through it, so the right body needs the hole to break through a \
neighbouring face, a topology change this operation does not make. \
`validate()` cannot see it (every incidence is intact), the \
self-intersection scan cannot (nothing crosses), and the volume need not \
change at all — refusing rather than returning the wrong body",
breach.loop_id, where_in_space,
),
)
}
struct BreakoutPair {
wall: u64,
neighbour: u64,
}
fn breakout_pair(solid: &BrepSolid, breach: &HoleBreach) -> Result<BreakoutPair, KernelRefusal> {
let wall = breakout_wall(solid, breach)?;
match breakout_neighbours_crossed(solid, breach, wall)?[..] {
[neighbour] => Ok(BreakoutPair { wall, neighbour }),
[] => Err(KernelRefusal::unsupported(KernelStage::Validate, NOT_A_STRADDLE_WHAT, NOT_A_STRADDLE)),
ref many => Err(KernelRefusal::unsupported(KernelStage::Validate, "breakout.pair", format!(
"the runaway hole crosses into {} neighbouring faces at once ({many:?}) — a \
breakout over a corner, where the two re-trims bound each other and resolving one \
at a time is a different problem from resolving both",
many.len(),
))),
}
}
const NOT_A_STRADDLE: &str = "the hole does not straddle its own face's boundary";
const NOT_A_STRADDLE_WHAT: &str = "breakout.not_a_straddle";
fn is_not_a_straddle(refusal: &KernelRefusal) -> bool {
matches!(
&refusal.class,
crate::RefusalClass::UnsupportedGeometry { what } if what == NOT_A_STRADDLE_WHAT
)
}
fn breakout_wall(solid: &BrepSolid, breach: &HoleBreach) -> Result<u64, KernelRefusal> {
let face = face_by_id(solid, breach.face)?;
let mut walls: Vec<u64> = Vec::new();
for coedge in &face.loops[breach.loop_index].coedges {
for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
if other.id == face.id {
continue;
}
if other
.loops
.iter()
.any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
&& !walls.contains(&other.id)
{
walls.push(other.id);
}
}
}
match walls[..] {
[wall] => Ok(wall),
_ => Err(KernelRefusal::unsupported(KernelStage::Validate, "breakout.wall_count", format!(
"the runaway hole loop bounds {} face(s) ({walls:?}); this route re-trims ONE moved \
carrier against the body and cannot decide which of several is the one that moved",
walls.len(),
))),
}
}
fn face_by_id<'a>(
solid: &'a BrepSolid,
id: u64,
) -> Result<&'a crate::FaceRecord, KernelRefusal> {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == id)
.ok_or_else(|| {
KernelRefusal::internal(KernelStage::Validate, "face_by_id", format!("face {id} is not on the body"))
})
}
fn breakout_neighbours_crossed(
solid: &BrepSolid,
breach: &HoleBreach,
wall: u64,
) -> Result<Vec<u64>, KernelRefusal> {
let face = face_by_id(solid, breach.face)?;
let polylines = face
.loops
.iter()
.map(loop_parameter_polyline)
.collect::<Result<Vec<_>, KernelRefusal>>()
.map_err(|error| {
error.with_message(|error| format!("the breached face's loops could not be traced: {error}"))
})?;
let outer_index = (0..polylines.len())
.max_by(|&a, &b| {
polyline_area(&polylines[a])
.abs()
.total_cmp(&polyline_area(&polylines[b]).abs())
})
.unwrap_or(0);
if outer_index == breach.loop_index {
return Err(KernelRefusal::internal(KernelStage::Validate, "breakout.outer_loop", "the convicted loop is the face's own outer loop"));
}
let hole = &polylines[breach.loop_index];
let mut neighbours: Vec<u64> = Vec::new();
for coedge in &face.loops[outer_index].coedges {
let side = coedge_parameter_polyline(coedge)
.map_err(|error| error.with_message(|error| format!("an outer coedge could not be traced: {error}")))?;
if !polylines_cross(hole, &side) {
continue;
}
for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
if other.id == face.id || other.id == wall {
continue;
}
if other
.loops
.iter()
.any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
&& !neighbours.contains(&other.id)
{
neighbours.push(other.id);
}
}
}
Ok(neighbours)
}
fn faces_over_the_outer_boundary(
solid: &BrepSolid,
breach: &HoleBreach,
wall: u64,
) -> Result<Vec<u64>, KernelRefusal> {
let face = face_by_id(solid, breach.face)?;
let polylines = face
.loops
.iter()
.map(loop_parameter_polyline)
.collect::<Result<Vec<_>, KernelRefusal>>()
.unwrap_or_default();
let outer_index = (0..polylines.len())
.max_by(|&a, &b| {
polyline_area(&polylines[a])
.abs()
.total_cmp(&polyline_area(&polylines[b]).abs())
})
.unwrap_or(0);
let mut over: Vec<u64> = Vec::new();
for coedge in &face.loops[outer_index].coedges {
for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
if other.id == face.id || other.id == wall {
continue;
}
if other
.loops
.iter()
.any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
&& !over.contains(&other.id)
{
over.push(other.id);
}
}
}
Ok(over)
}
fn coedge_parameter_polyline(
coedge: &crate::topology::CoedgeRecord,
) -> Result<Vec<(f64, f64)>, KernelRefusal> {
let pcurve = &coedge.pcurve;
let [d0, d1] = pcurve.domain().or_refuse(KernelStage::Validate, "domain")?;
let steps = if pcurve.degree <= 1 && pcurve.control_points.len() == 2 {
1
} else {
let mut spans = 0usize;
for window in pcurve.knots.windows(2) {
if window[1] > window[0] {
spans += 1;
}
}
spans.max(1) * LOOP_SAMPLES_PER_SPAN
};
let mut points = Vec::with_capacity(steps + 1);
for step in 0..=steps {
let uv = pcurve.evaluate(d0 + (d1 - d0) * (step as f64 / steps as f64)).or_refuse(KernelStage::Validate, "evaluate")?;
points.push((uv.x, uv.y));
}
Ok(points)
}
fn polylines_cross(loop_points: &[(f64, f64)], side: &[(f64, f64)]) -> bool {
for index in 0..loop_points.len() {
let a = loop_points[index];
let b = loop_points[(index + 1) % loop_points.len()];
for window in side.windows(2) {
if segments_cross(a, b, window[0], window[1]) {
return true;
}
}
}
false
}
fn segments_cross(a: (f64, f64), b: (f64, f64), c: (f64, f64), d: (f64, f64)) -> bool {
let orient = |p: (f64, f64), q: (f64, f64), r: (f64, f64)| {
(q.0 - p.0) * (r.1 - p.1) - (q.1 - p.1) * (r.0 - p.0)
};
let (d1, d2) = (orient(c, d, a), orient(c, d, b));
let (d3, d4) = (orient(a, b, c), orient(a, b, d));
((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0))
}
fn repair_hole_breakout(solid: &BrepSolid, breach: &HoleBreach) -> Result<BrepSolid, KernelRefusal> {
let origin = || RepairOrigin::Breakout {
face: breach.face,
loop_id: breach.loop_id,
};
if breach.banded {
return Err(KernelRefusal::unsupported(
KernelStage::Validate,
"breakout.band_face",
format!(
"face {} is a band bounded by rings that wind its carrier's period, with no outer \
loop in its parameter plane; this route re-trims a hole against the outer loop \
it straddles and does not re-trim a band",
breach.face,
),
));
}
match breakout_pair(solid, breach) {
Ok(BreakoutPair { wall, neighbour }) => {
return split_and_drop(solid, wall, neighbour, origin())
}
Err(reason) if !is_not_a_straddle(&reason) => {
return Err(KernelRefusal::unsupported(
KernelStage::Validate,
"breakout.pair",
reason.message,
))
}
Err(_) => {}
}
let wall = breakout_wall(solid, breach).map_err(|refusal| {
KernelRefusal::internal(KernelStage::Validate, "breakout.wall", refusal.message)
})?;
let mut candidates = faces_over_the_outer_boundary(solid, breach, wall)
.map_err(|refusal| {
KernelRefusal::internal(KernelStage::Validate, "breakout.candidates", refusal.message)
})?;
if candidates.is_empty() {
return Err(KernelRefusal::unsupported(
KernelStage::Validate,
"breakout.runaway",
RUNAWAY,
));
}
let mouth = hole_loop_in_space(solid, breach).map_err(|refusal| {
KernelRefusal::internal(KernelStage::Validate, "breakout.mouth", refusal.message)
})?;
let scale = crate::solid_scale(solid);
let mut ranked: Vec<(f64, u64)> = Vec::new();
for candidate in candidates.drain(..) {
ranked.push((
distance_to_face(solid, candidate, &mouth)
.map_err(|refusal| {
KernelRefusal::internal(KernelStage::Validate, "breakout.distance", refusal.message)
})?,
candidate,
));
}
ranked.sort_by(|a, b| a.0.total_cmp(&b.0));
if ranked.len() > 1 && (ranked[1].0 - ranked[0].0).abs() <= scale * 1e-9 {
return Err(KernelRefusal::ill_posed(
KernelStage::Validate,
"breakout.corner_tie",
format!(
"the migrated mouth is the same distance ({:.6}) from faces {} and {}, so the body \
does not say which of them it moved onto — a breakout over the corner between \
them, where the two re-trims bound each other",
ranked[0].0, ranked[0].1, ranked[1].1,
),
));
}
let carried = take_crossing_repairs();
let mut declined: Vec<String> = Vec::new();
for (distance, candidate) in ranked {
match split_and_drop(solid, wall, candidate, origin()) {
Ok(body) => {
let mine = take_crossing_repairs();
restore_repairs(carried.into_iter().chain(mine).collect());
return Ok(body);
}
Err(reason) => {
declined.push(format!("face {candidate} at {distance:.6}: {reason}"))
}
}
}
restore_repairs(carried);
Err(KernelRefusal::unsupported(
KernelStage::Validate,
"breakout.runaway",
format!(
"{RUNAWAY} — the moved carrier was offered every face across that boundary, nearest \
first, and cut none of them ({})",
declined.join(" | ")
),
))
}
fn hole_loop_in_space(solid: &BrepSolid, breach: &HoleBreach) -> Result<Vec<crate::Vec3>, KernelRefusal> {
let face = face_by_id(solid, breach.face)?;
let uv = loop_parameter_polyline(&face.loops[breach.loop_index])?;
uv.into_iter()
.map(|(u, v)| face.surface.evaluate(u, v))
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("the convicted hole loop could not be read in space: {error}"))
.or_refuse(KernelStage::Validate, "evaluate")
}
fn distance_to_face(
solid: &BrepSolid,
face_id: u64,
points: &[crate::Vec3],
) -> Result<f64, KernelRefusal> {
const STATIONS: usize = 32;
let face = face_by_id(solid, face_id)?;
let polylines = face
.loops
.iter()
.map(loop_parameter_polyline)
.collect::<Result<Vec<_>, KernelRefusal>>()?;
let outer_index = (0..polylines.len())
.max_by(|&a, &b| {
polyline_area(&polylines[a])
.abs()
.total_cmp(&polyline_area(&polylines[b]).abs())
})
.unwrap_or(0);
let bands = BoundaryBands::build(&polylines[outer_index]);
let (mut lo, mut hi) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));
for &(u, v) in &polylines[outer_index] {
lo = (lo.0.min(u), lo.1.min(v));
hi = (hi.0.max(u), hi.1.max(v));
}
let mut best = f64::INFINITY;
for iu in 0..=STATIONS {
for iv in 0..=STATIONS {
let u = lo.0 + (hi.0 - lo.0) * iu as f64 / STATIONS as f64;
let v = lo.1 + (hi.1 - lo.1) * iv as f64 / STATIONS as f64;
if !bands.contains((u, v)) {
continue;
}
if polylines
.iter()
.enumerate()
.any(|(index, hole)| index != outer_index && inside_polyline((u, v), hole))
{
continue;
}
let Ok(point) = face.surface.evaluate(u, v) else {
continue;
};
for probe in points {
best = best.min(probe.sub(point).length());
}
}
}
Ok(best)
}
fn inside_polyline(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
let (u, v) = point;
let mut parity = false;
for index in 0..polygon.len() {
let (ax, ay) = polygon[index];
let (bx, by) = polygon[(index + 1) % polygon.len()];
if (ay > v) != (by > v) {
let x = ax + (v - ay) / (by - ay) * (bx - ax);
if x > u {
parity = !parity;
}
}
}
parity
}
const RUNAWAY: &str = "the runaway hole has left the face it was recorded on without arriving \
on another: the moved carrier meets no face of the body, so there is nothing for it to be \
re-trimmed against and no cut to perform at any extent";
pub fn resolve_face_crossing_between(
solid: &BrepSolid,
face_a: u64,
face_b: u64,
) -> Result<BrepSolid, KernelRefusal> {
let report = scan(solid, "resolve_face_crossing_between")?;
split_and_drop(solid, face_a, face_b, RepairOrigin::Scan(&report))
}
enum RepairOrigin<'a> {
Scan(&'a SelfIntersectionReport),
Breakout { face: u64, loop_id: u64 },
}
const SELF_OPERAND: u8 = 0;
const MAXIMUM_CROSSING_PIECES: usize = 8;
pub fn resolve_face_crossing(solid: &BrepSolid) -> Result<BrepSolid, KernelRefusal> {
let options = SelfIntersectionOptions::for_solid(solid);
let report = solid_self_intersections(solid, options).map_err(|message| {
KernelRefusal::internal(
KernelStage::Validate,
"soundness_scan",
format!("the soundness scan could not run: {message}"),
)
})?;
if let Some(fold) = report.folds.first() {
return Err(KernelRefusal::unsound(
KernelStage::Validate,
SoundnessDefect::Fold,
report.folds.iter().map(|fold| fold.face).collect(),
format!(
"face {} folds through ITSELF; a fold is a trim that was never constructed and \
belongs to the carrier carve, not to this repair",
fold.face
),
));
}
if report.confirmed.is_empty() {
return Ok(solid.clone());
}
if report.confirmed.len() > 1 {
return Err(KernelRefusal::unsupported(
KernelStage::Validate,
"crossing.multiple_pairs",
format!(
"{} crossing face pairs; each repair changes the faces the next would be measured \
against, so this repair takes one pair at a time",
report.confirmed.len()
),
));
}
let crossing = &report.confirmed[0];
split_and_drop(solid, crossing.face_a, crossing.face_b, RepairOrigin::Scan(&report))
}
fn split_and_drop(
solid: &BrepSolid,
face_a: u64,
face_b: u64,
origin: RepairOrigin<'_>,
) -> Result<BrepSolid, KernelRefusal> {
use crate::fragment::fragment_solid;
use crate::imprint::{FaceKey, ImprintOptions};
use crate::{apply_edge_splits, build_imprints};
let scale = crate::solid_scale(solid);
let tolerance = (scale * 1e-7).max(1e-9);
if trace_on() {
eprintln!("HEAL split_and_drop: faces {face_a} | {face_b}, scale {scale:.6}");
trace_solid("entry solid", solid);
}
let open_a = one_face_body(solid, face_a)?;
let open_b = one_face_body(solid, face_b)?;
let mut request = ImprintOptions {
tolerance,
local_fit: false,
..ImprintOptions::default()
};
measured_fit_request(&mut request);
let mut imprint = build_imprints(&open_a, &open_b, &request).map_err(|error| {
error.with_message(|error| format!("the two carriers could not be imprinted: {error}"))
})?;
for face in &mut imprint.by_face {
face.operand = SELF_OPERAND;
}
for split in &mut imprint.edge_splits {
split.operand = SELF_OPERAND;
}
for piece in &mut imprint.pieces {
for support in &mut piece.support_faces {
*support = FaceKey {
operand: SELF_OPERAND,
face_id: support.face_id,
};
}
for pcurve in &mut piece.pcurves {
pcurve.operand = SELF_OPERAND;
}
}
for barrier in &mut imprint.barrier_edges {
barrier.0 = SELF_OPERAND;
}
if trace_on() {
trace_imprint(&imprint);
}
let cut_a = imprint
.by_face
.iter()
.any(|face| face.face_id == face_a && !face.piece_ids.is_empty());
let cut_b = imprint
.by_face
.iter()
.any(|face| face.face_id == face_b && !face.piece_ids.is_empty());
if !cut_a || !cut_b {
return Err(KernelRefusal::unsupported(
KernelStage::Fragment,
"crossing.no_shared_curve",
format!(
"the carriers' intersection reaches {} of the two trims, so there is no shared \
curve to split along",
match (cut_a, cut_b) {
(true, false) => format!("only face {face_a}"),
(false, true) => format!("only face {face_b}"),
_ => "neither".into(),
}
),
));
}
let split = apply_edge_splits(solid, SELF_OPERAND, &imprint).map_err(|error| {
error.with_message(|error| format!("the imprinted edge splits could not be applied: {error}"))
})?;
let fragments = fragment_solid(&split, SELF_OPERAND, &imprint).map_err(|error| {
error.with_message(|error| format!("the split faces could not be fragmented: {error}"))
})?;
if trace_on() {
trace_solid("split solid", &split);
trace_fragments(&fragments);
}
let touched = touched_faces(solid, &imprint, face_a, face_b);
let degenerate: rustc_hash::FxHashSet<EdgeKey> = split
.edges
.iter()
.filter(|edge| edge.degenerate)
.map(|edge| EdgeKey::Boundary(SELF_OPERAND, edge.id))
.collect();
let pieces_of = |face: u64| -> Vec<usize> {
fragments
.iter()
.enumerate()
.filter_map(|(index, fragment)| (fragment.source_face_id == face).then_some(index))
.collect()
};
let pieces_a = pieces_of(face_a);
let pieces_b = pieces_of(face_b);
let (need_a, need_b) = match &origin {
RepairOrigin::Scan(_) => (2, 2),
RepairOrigin::Breakout { .. } => {
if pieces_a.len().max(pieces_b.len()) >= 2 {
(1, 1)
} else {
(2, 2)
}
}
};
if pieces_a.len() < need_a || pieces_b.len() < need_b {
return Err(KernelRefusal::unsupported(
KernelStage::Fragment,
"crossing.one_sided_cut",
format!(
"the crossing cut face {face_a} into {} piece(s) and face {face_b} into {} — this \
repair resolves the case where the curve cuts BOTH trims, so one part of each is \
the tail that overshot",
pieces_a.len(),
pieces_b.len()
),
));
}
if pieces_a.len() + pieces_b.len() > MAXIMUM_CROSSING_PIECES {
return Err(KernelRefusal::non_convergence(
KernelStage::Fragment,
"crossing.pieces",
format!(
"the crossing cut face {face_a} into {} piece(s) and face {face_b} into {}, which is \
{} pieces against this repair's budget of {MAXIMUM_CROSSING_PIECES} — every way to \
drop some of them is rebuilt and re-checked, so the enumeration is bounded rather \
than sampled",
pieces_a.len(),
pieces_b.len(),
pieces_a.len() + pieces_b.len(),
),
));
}
let plan = DropPlan {
split: &split,
fragments: &fragments,
imprint: &imprint,
touched: &touched,
degenerate: °enerate,
pieces_a: &pieces_a,
pieces_b: &pieces_b,
face_a,
face_b,
scale,
};
let (mut verified, refusals) = plan.enumerate(DropClosure::Drop);
if verified.is_empty() {
verified = plan.enumerate(DropClosure::Retrim).0;
}
if verified.len() > 1 {
return Err(KernelRefusal::ill_posed(
KernelStage::Select,
"crossing.ambiguous_drop",
format!(
"{} different drops of the crossing pieces all rebuilt into a sound body, so the \
result does not decide which material the crossing encloses; this repair answers \
only where one does",
verified.len()
),
));
}
let Some(Candidate {
dropped,
retired,
retrimmed,
solid: repaired,
}) = verified.pop()
else {
let listed = refusals
.iter()
.take(4)
.cloned()
.collect::<Vec<_>>()
.join(" | ");
return Err(KernelRefusal::unsound(
KernelStage::Sew,
SoundnessDefect::Crossing,
vec![face_a, face_b],
format!(
"none of the {} ways of dropping the crossing pieces rebuilt into a sound body \
({listed}{}); the worst crossing was {:.3e} past its band, so the overshoot is \
real and it is the SPLIT that did not resolve it",
refusals.len(),
if refusals.len() > 4 {
format!(" | and {} more", refusals.len() - 4)
} else {
String::new()
},
match &origin {
RepairOrigin::Scan(report) => report
.confirmed
.first()
.map(|crossing| crossing.straddle)
.unwrap_or_default(),
RepairOrigin::Breakout { .. } => 0.0,
}
),
));
};
record_repair(
&fragments,
&dropped,
&retired,
&retrimmed,
face_a,
face_b,
&origin,
);
Ok(repaired)
}
struct Candidate {
dropped: Vec<usize>,
retired: Vec<u64>,
retrimmed: Vec<u64>,
solid: BrepSolid,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum DropClosure {
Drop,
Retrim,
}
struct DropPlan<'a> {
split: &'a BrepSolid,
fragments: &'a [crate::fragment::FaceFragmentRecord],
imprint: &'a crate::imprint::ImprintResultRecord,
touched: &'a rustc_hash::FxHashSet<u64>,
degenerate: &'a rustc_hash::FxHashSet<EdgeKey>,
pieces_a: &'a [usize],
pieces_b: &'a [usize],
face_a: u64,
face_b: u64,
scale: f64,
}
impl DropPlan<'_> {
fn enumerate(&self, closure: DropClosure) -> (Vec<Candidate>, Vec<String>) {
let (face_a, face_b) = (self.face_a, self.face_b);
let mut refusals: Vec<String> = Vec::new();
let mut verified: Vec<Candidate> = Vec::new();
let mut seen: Vec<(Vec<usize>, Vec<Vec<usize>>)> = Vec::new();
for drop_a in proper_subsets(self.pieces_a) {
for drop_b in proper_subsets(self.pieces_b) {
let mut dropped: Vec<usize> =
drop_a.iter().chain(drop_b.iter()).copied().collect();
if dropped.is_empty() {
continue;
}
let label = format!(
"dropping piece(s) {:?} of face {face_a} and {:?} of face {face_b}",
drop_a
.iter()
.map(|index| self
.pieces_a
.iter()
.position(|piece| piece == index)
.unwrap_or(0))
.collect::<Vec<_>>(),
drop_b
.iter()
.map(|index| self
.pieces_b
.iter()
.position(|piece| piece == index)
.unwrap_or(0))
.collect::<Vec<_>>(),
);
let mut retired: Vec<u64> = Vec::new();
let closed = match closure {
DropClosure::Drop => close_drop_set(
self.fragments,
&mut dropped,
&mut retired,
self.touched,
self.degenerate,
)
.map(|()| (None, Vec::new())),
DropClosure::Retrim => retrim_drop_set(
self.fragments,
&mut dropped,
&mut retired,
self.touched,
self.degenerate,
)
.map(|(working, retrimmed)| (Some(working), retrimmed)),
};
let (working, retrimmed) = match closed {
Ok(closed) => closed,
Err(reason) => {
if trace_on() {
println!(" candidate died at closure {label}: {reason}");
}
refusals.push(format!("{label}: {reason}"));
continue;
}
};
let working: &[crate::fragment::FaceFragmentRecord] =
working.as_deref().unwrap_or(self.fragments);
dropped.sort_unstable();
let signature = (
dropped.clone(),
working
.iter()
.enumerate()
.filter(|(index, _)| !dropped.contains(index))
.map(|(_, fragment)| {
fragment
.loops
.iter()
.map(|fragment_loop| fragment_loop.coedges.len())
.collect()
})
.collect(),
);
if seen.contains(&signature) {
continue;
}
seen.push(signature);
if dropped.len() == working.len() {
refusals.push(format!("{label}: the re-trim consumed every face"));
continue;
}
if let Some((key, count)) = over_used_edge(working, &dropped, self.degenerate) {
refusals.push(format!(
"{label}: {} is held {count} times by the pieces this drop keeps, and a \
closed body holds every edge exactly twice",
match key {
EdgeKey::Boundary(_, edge) => format!("edge {edge}"),
EdgeKey::Imprint(piece) => format!("imprint piece {piece}"),
}
));
if trace_on() {
println!(
" candidate refused {label}: {key:?} held {count} times"
);
}
continue;
}
let selected: Vec<_> = working
.iter()
.enumerate()
.filter(|(index, _)| !dropped.contains(index))
.map(|(_, fragment)| fragment.clone())
.collect();
match rebuild(self.split, selected, self.imprint, self.scale) {
Ok(solid) => {
if trace_on() {
println!(
" CANDIDATE VERIFIED {label} volume {:.6} dropped {:?}",
crate::solid_mass_properties(&solid)
.map(|mass| mass.volume)
.unwrap_or(f64::NAN),
dropped
);
}
verified.push(Candidate {
dropped,
retired,
retrimmed,
solid,
})
}
Err(reason) => {
if trace_on() {
println!(" candidate refused {label}: {reason}");
}
refusals.push(format!("{label}: {reason}"))
}
}
}
}
(verified, refusals)
}
}
fn touched_faces(
solid: &BrepSolid,
imprint: &crate::imprint::ImprintResultRecord,
face_a: u64,
face_b: u64,
) -> rustc_hash::FxHashSet<u64> {
use rustc_hash::FxHashSet as HashSet;
let split_edges: HashSet<u64> = imprint
.edge_splits
.iter()
.filter(|split| split.operand == SELF_OPERAND)
.map(|split| split.edge_id)
.collect();
let mut touched: HashSet<u64> = [face_a, face_b].into_iter().collect();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
let uses_split = face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| split_edges.contains(&coedge.edge_id));
if uses_split {
touched.insert(face.id);
}
}
touched
}
fn close_drop_set(
fragments: &[crate::fragment::FaceFragmentRecord],
dropped: &mut Vec<usize>,
retired: &mut Vec<u64>,
touched: &rustc_hash::FxHashSet<u64>,
degenerate: &rustc_hash::FxHashSet<EdgeKey>,
) -> Result<(), KernelRefusal> {
use rustc_hash::FxHashMap as HashMap;
loop {
let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
for (index, fragment) in fragments.iter().enumerate() {
if dropped.contains(&index) {
continue;
}
for key in fragment
.loops
.iter()
.flat_map(|fragment_loop| &fragment_loop.coedges)
.filter_map(|coedge| edge_key(&coedge.source))
{
*uses.entry(key).or_default() += 1;
}
}
let mut grew = false;
for (index, fragment) in fragments.iter().enumerate() {
if dropped.contains(&index) {
continue;
}
if fragment_is_detached(fragment, &uses, degenerate) {
dropped.push(index);
retired.push(fragment.source_face_id);
grew = true;
continue;
}
let unmatched = fragment
.loops
.iter()
.flat_map(|fragment_loop| &fragment_loop.coedges)
.filter_map(|coedge| edge_key(&coedge.source))
.filter(|key| !degenerate.contains(key))
.any(|key| uses.get(&key) == Some(&1));
if !unmatched {
continue;
}
if !touched.contains(&fragment.source_face_id) {
return Err(KernelRefusal::internal(KernelStage::Validate, "retrim.uncut_face", format!(
"the re-trim reached face {}, which the imprint never cut — the drop has run \
past the material the crossing encloses",
fragment.source_face_id
)));
}
dropped.push(index);
grew = true;
}
if !grew {
return Ok(());
}
}
}
fn retrim_drop_set(
fragments: &[crate::fragment::FaceFragmentRecord],
dropped: &mut Vec<usize>,
retired: &mut Vec<u64>,
touched: &rustc_hash::FxHashSet<u64>,
degenerate: &rustc_hash::FxHashSet<EdgeKey>,
) -> Result<(Vec<crate::fragment::FaceFragmentRecord>, Vec<u64>), KernelRefusal> {
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
let mut working = fragments.to_vec();
let mut retrimmed: Vec<u64> = Vec::new();
loop {
let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
for (index, fragment) in working.iter().enumerate() {
if dropped.contains(&index) {
continue;
}
for key in fragment
.loops
.iter()
.flat_map(|fragment_loop| &fragment_loop.coedges)
.filter_map(|coedge| edge_key(&coedge.source))
{
*uses.entry(key).or_default() += 1;
}
}
let mut grew = false;
for index in 0..working.len() {
if dropped.contains(&index) {
continue;
}
if fragment_is_detached(&working[index], &uses, degenerate) {
dropped.push(index);
retired.push(working[index].source_face_id);
grew = true;
}
}
if grew {
continue;
}
let unmatched: HashSet<EdgeKey> = uses
.iter()
.filter(|(key, count)| **count == 1 && !degenerate.contains(key))
.map(|(key, _)| *key)
.collect();
if unmatched.is_empty() {
return Ok((working, retrimmed));
}
for index in 0..working.len() {
if dropped.contains(&index) {
continue;
}
let holds = working[index]
.loops
.iter()
.flat_map(|fragment_loop| &fragment_loop.coedges)
.filter_map(|coedge| edge_key(&coedge.source))
.any(|key| unmatched.contains(&key));
if !holds {
continue;
}
let stale = holds_only_stale_loops(&working[index], &unmatched, degenerate);
if !touched.contains(&working[index].source_face_id) && !stale {
return Err(KernelRefusal::internal(KernelStage::Validate, "retrim.uncut_face", format!(
"the re-trim reached face {}, which the imprint never cut — the drop has run \
past the material the crossing encloses",
working[index].source_face_id
)));
}
let face = working[index].source_face_id;
match retrim_fragment(&working[index], &unmatched)? {
Some(next) => {
working[index] = next;
if stale {
retired.push(face);
} else if !retrimmed.contains(&face) {
retrimmed.push(face);
}
}
None => dropped.push(index),
}
grew = true;
}
if !grew {
return Ok((working, retrimmed));
}
}
}
fn retrim_fragment(
fragment: &crate::fragment::FaceFragmentRecord,
remove: &rustc_hash::FxHashSet<EdgeKey>,
) -> Result<Option<crate::fragment::FaceFragmentRecord>, KernelRefusal> {
use crate::fragment::{FaceFragmentRecord, FragmentCoedge, FragmentLoop};
let kept: Vec<FragmentCoedge> = fragment
.loops
.iter()
.flat_map(|fragment_loop| &fragment_loop.coedges)
.filter(|coedge| !edge_key(&coedge.source).is_some_and(|key| remove.contains(&key)))
.cloned()
.collect();
if kept.is_empty() {
return Ok(None);
}
let ends = kept
.iter()
.map(|coedge| {
let [t0, t1] = coedge.pcurve.domain()?;
Ok((coedge.pcurve.evaluate(t0)?, coedge.pcurve.evaluate(t1)?))
})
.collect::<Result<Vec<_>, String>>().or_refuse(KernelStage::Validate, "collect")?;
let [u0, u1] = fragment.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
let [v0, v1] = fragment.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
let tolerance = 1e-4 * (u1 - u0).hypot(v1 - v0);
let mut used = vec![false; kept.len()];
let mut loops: Vec<FragmentLoop> = Vec::new();
for seed in 0..kept.len() {
if used[seed] {
continue;
}
used[seed] = true;
let mut chain = vec![seed];
let start = ends[seed].0;
let mut cursor = ends[seed].1;
while cursor.sub(start).length() > tolerance {
let next = (0..kept.len())
.filter(|index| !used[*index])
.map(|index| (index, ends[index].0.sub(cursor).length()))
.filter(|(_, gap)| *gap <= tolerance)
.min_by(|left, right| left.1.total_cmp(&right.1))
.map(|(index, _)| index);
let Some(next) = next else {
return Err(KernelRefusal::internal(KernelStage::Validate, "retrim.open_chain", format!(
"face {}'s re-trim left a chain open at ({:.6}, {:.6}) in its own parameter \
domain — what the drop took off it does not close into a loop",
fragment.source_face_id, cursor.x, cursor.y
)));
};
used[next] = true;
chain.push(next);
cursor = ends[next].1;
}
loops.push(FragmentLoop {
coedges: chain.into_iter().map(|index| kept[index].clone()).collect(),
});
}
Ok(Some(FaceFragmentRecord {
loops,
..fragment.clone()
}))
}
fn fragment_is_detached(
fragment: &crate::fragment::FaceFragmentRecord,
uses: &rustc_hash::FxHashMap<EdgeKey, usize>,
degenerate: &rustc_hash::FxHashSet<EdgeKey>,
) -> bool {
use rustc_hash::FxHashMap as HashMap;
let mut own: HashMap<EdgeKey, usize> = HashMap::default();
for key in fragment
.loops
.iter()
.flat_map(|fragment_loop| &fragment_loop.coedges)
.filter_map(|coedge| edge_key(&coedge.source))
.filter(|key| !degenerate.contains(key))
{
*own.entry(key).or_default() += 1;
}
!own.is_empty()
&& own
.iter()
.all(|(key, count)| uses.get(key).copied().unwrap_or(0) == *count)
}
fn holds_only_stale_loops(
fragment: &crate::fragment::FaceFragmentRecord,
unmatched: &rustc_hash::FxHashSet<EdgeKey>,
degenerate: &rustc_hash::FxHashSet<EdgeKey>,
) -> bool {
if !matches!(
fragment.surface.analytic(),
Some(crate::AnalyticSurface::Plane { .. })
) {
return false;
}
let mut stale: Vec<usize> = Vec::new();
let mut surviving = 0usize;
for (index, fragment_loop) in fragment.loops.iter().enumerate() {
let keys: Vec<EdgeKey> = fragment_loop
.coedges
.iter()
.filter_map(|coedge| edge_key(&coedge.source))
.filter(|key| !degenerate.contains(key))
.collect();
if !keys.is_empty() && keys.iter().all(|key| unmatched.contains(key)) {
stale.push(index);
continue;
}
if keys.iter().any(|key| unmatched.contains(key)) {
return false;
}
surviving += 1;
}
if stale.is_empty() || surviving == 0 {
return false;
}
let Ok(polylines) = fragment
.loops
.iter()
.map(fragment_loop_polyline)
.collect::<Result<Vec<_>, KernelRefusal>>()
else {
return false;
};
if polylines.iter().any(Vec::is_empty) {
return false;
}
let outer_index = (0..polylines.len())
.max_by(|&a, &b| {
polyline_area(&polylines[a])
.abs()
.total_cmp(&polyline_area(&polylines[b]).abs())
})
.unwrap_or(0);
if stale.contains(&outer_index) {
return false;
}
let outer = &polylines[outer_index];
let bands = BoundaryBands::build(outer);
let tolerance = boundary_tolerance(outer);
stale.iter().all(|index| {
polylines[*index]
.iter()
.all(|&point| !bands.contains(point) && !bands.within(point, tolerance))
})
}
fn over_used_edge(
fragments: &[crate::fragment::FaceFragmentRecord],
dropped: &[usize],
degenerate: &rustc_hash::FxHashSet<EdgeKey>,
) -> Option<(EdgeKey, usize)> {
use rustc_hash::FxHashMap as HashMap;
let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
for (index, fragment) in fragments.iter().enumerate() {
if dropped.contains(&index) {
continue;
}
for key in fragment
.loops
.iter()
.flat_map(|fragment_loop| &fragment_loop.coedges)
.filter_map(|coedge| edge_key(&coedge.source))
.filter(|key| !degenerate.contains(key))
{
*uses.entry(key).or_default() += 1;
}
}
let mut over: Vec<(EdgeKey, usize)> = uses
.into_iter()
.filter(|(_, count)| *count > 2)
.collect();
over.sort_unstable();
over.into_iter().next()
}
fn edge_key(source: &crate::fragment::FragmentEdgeSource) -> Option<EdgeKey> {
use crate::fragment::FragmentEdgeSource;
match source {
FragmentEdgeSource::Boundary { operand, edge_id }
| FragmentEdgeSource::SharedBoundary { operand, edge_id } => {
Some(EdgeKey::Boundary(*operand, *edge_id))
}
FragmentEdgeSource::Imprint { piece_id } => Some(EdgeKey::Imprint(*piece_id)),
FragmentEdgeSource::Derived { .. } => None,
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
enum EdgeKey {
Boundary(u8, u64),
Imprint(u64),
}
fn proper_subsets(pieces: &[usize]) -> Vec<Vec<usize>> {
let mut subsets = Vec::new();
for mask in 0..(1u32 << pieces.len()) {
if mask == (1u32 << pieces.len()) - 1 {
continue;
}
subsets.push(
pieces
.iter()
.enumerate()
.filter(|(bit, _)| mask & (1 << bit) != 0)
.map(|(_, piece)| *piece)
.collect(),
);
}
subsets
}
fn rebuild(
split: &BrepSolid,
selected: Vec<crate::fragment::FaceFragmentRecord>,
imprint: &crate::imprint::ImprintResultRecord,
scale: f64,
) -> Result<BrepSolid, KernelRefusal> {
use crate::boolean::{assemble_open_fragments, finalize_assembled_solid};
use rustc_hash::FxHashMap as HashMap;
let mut sources: HashMap<u8, &BrepSolid> = HashMap::default();
sources.insert(SELF_OPERAND, split);
let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
let solid = assemble_open_fragments(selected, &sources, imprint, assembly_tolerance)
.map_err(|error| error.with_message(|error| format!("the kept pieces did not assemble: {error}")))?;
let solid = finalize_assembled_solid(solid, assembly_tolerance)
.map_err(|error| error.with_message(|error| format!("the assembly did not finalize: {error}")))?;
let issues = solid.validate();
if !issues.is_empty() {
return Err(KernelRefusal::internal(KernelStage::Validate, "rebuild.validate", format!(
"the rebuilt body fails validate(): {}",
issues[0].message
)));
}
if crate::solid_connectivity(&solid).is_disconnected() {
return Err(KernelRefusal::internal(KernelStage::Validate, "rebuild.connected", "the rebuilt body is not connected"));
}
let options = SelfIntersectionOptions::for_solid(&solid);
let report = solid_self_intersections(&solid, options)
.map_err(|message| format!("the rebuilt body could not be scanned: {message}"))
.or_refuse(KernelStage::Validate, "solid_self_intersections")?;
if report.is_flagged() {
return Err(KernelRefusal::internal(KernelStage::Validate, "rebuild.self_intersects", format!(
"the rebuilt body still self-intersects: {}",
report.summary().unwrap_or_else(|| "no summary".into())
)));
}
Ok(solid)
}
fn one_face_body(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, KernelRefusal> {
use crate::topology::ShellRecord;
use rustc_hash::FxHashSet as HashSet;
let face = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == face_id)
.ok_or_else(|| {
KernelRefusal::internal(KernelStage::Fragment, "one_face_body", format!("face {face_id} is not in this body"))
})?
.clone();
let used: HashSet<u64> = face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.map(|coedge| coedge.edge_id)
.collect();
let edges: Vec<_> = solid
.edges
.iter()
.filter(|edge| used.contains(&edge.id))
.cloned()
.collect();
let vertices: HashSet<u64> = edges
.iter()
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect();
Ok(BrepSolid {
mass_properties_cache: Default::default(),
id: solid.id,
genus: 0,
vertices: solid
.vertices
.iter()
.filter(|vertex| vertices.contains(&vertex.id))
.cloned()
.collect(),
edges,
shells: vec![ShellRecord {
id: 1,
faces: vec![face],
}],
})
}
#[derive(Clone, Debug)]
pub struct CrossingRepairRecord {
pub faces: (u64, u64),
pub point: crate::Vec3,
pub straddle: f64,
pub dropped: Vec<(u64, usize)>,
pub retired: Vec<(u64, usize)>,
pub retrimmed: Vec<u64>,
pub breakout: Option<(u64, u64)>,
}
impl std::fmt::Display for CrossingRepairRecord {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some((face, loop_id)) = self.breakout {
return write!(
formatter,
"repaired a BREAKOUT: hole loop {loop_id} of face {face} had left that face \
onto face {}, so face {} — the wall it bounds — and face {} were split along \
their carriers' intersection; dropped {} and re-trimmed {}{}",
self.faces.1,
self.faces.0,
self.faces.1,
self.pieces_dropped(),
self.faces_retrimmed(),
self.pieces_retired(),
);
}
write!(
formatter,
"repaired a self-crossing: faces {} and {} crossed near ({:.4}, {:.4}, {:.4}) \
(straddle {:.3e}); split along their carriers' intersection, dropped {} \
and re-trimmed {}{}",
self.faces.0,
self.faces.1,
self.point.x,
self.point.y,
self.point.z,
self.straddle,
self.pieces_dropped(),
self.faces_retrimmed(),
self.pieces_retired(),
)
}
}
impl CrossingRepairRecord {
fn pieces_dropped(&self) -> String {
self.dropped
.iter()
.map(|(face, count)| format!("{count} piece(s) of face {face}"))
.collect::<Vec<_>>()
.join(", ")
}
fn faces_retrimmed(&self) -> String {
if self.retrimmed.is_empty() {
"no neighbouring face".to_string()
} else {
format!(
"face(s) {}",
self.retrimmed
.iter()
.map(|face| face.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}
fn pieces_retired(&self) -> String {
if self.retired.is_empty() {
return String::new();
}
format!(
", and RETIRED {} the old configuration left, held by nothing else this repair keeps",
self.retired
.iter()
.map(|(face, count)| format!("{count} leftover(s) of face {face}"))
.collect::<Vec<_>>()
.join(", ")
)
}
}
thread_local! {
static CROSSING_REPAIRS: std::cell::RefCell<Vec<CrossingRepairRecord>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn take_crossing_repairs() -> Vec<CrossingRepairRecord> {
CROSSING_REPAIRS.with(|repairs| std::mem::take(&mut *repairs.borrow_mut()))
}
fn restore_repairs(records: Vec<CrossingRepairRecord>) {
CROSSING_REPAIRS.with(|repairs| *repairs.borrow_mut() = records);
}
fn record_repair(
fragments: &[crate::fragment::FaceFragmentRecord],
dropped: &[usize],
retired: &[u64],
retrimmed_faces: &[u64],
face_a: u64,
face_b: u64,
origin: &RepairOrigin<'_>,
) {
let tally = |faces: &[u64]| {
let mut by_face: Vec<(u64, usize)> = Vec::new();
for face in faces {
match by_face.iter_mut().find(|(existing, _)| existing == face) {
Some((_, count)) => *count += 1,
None => by_face.push((*face, 1)),
}
}
by_face.sort_unstable();
by_face
};
let by_face = tally(
&dropped
.iter()
.map(|index| fragments[*index].source_face_id)
.collect::<Vec<_>>(),
);
let retired = tally(retired);
let mut retrimmed: Vec<u64> = by_face
.iter()
.filter(|(face, count)| {
retired
.iter()
.find(|(retired_face, _)| retired_face == face)
.map(|(_, retired_count)| retired_count < count)
.unwrap_or(true)
})
.map(|(face, _)| *face)
.chain(retrimmed_faces.iter().copied())
.filter(|face| *face != face_a && *face != face_b)
.collect();
retrimmed.sort_unstable();
retrimmed.dedup();
let crossing = match origin {
RepairOrigin::Scan(report) => report.confirmed.first(),
RepairOrigin::Breakout { .. } => None,
};
let record = CrossingRepairRecord {
faces: (face_a, face_b),
point: crossing.map(|crossing| crossing.point).unwrap_or_default(),
straddle: crossing.map(|crossing| crossing.straddle).unwrap_or_default(),
dropped: by_face,
retired,
retrimmed,
breakout: match origin {
RepairOrigin::Scan(_) => None,
RepairOrigin::Breakout { face, loop_id } => Some((*face, *loop_id)),
},
};
if trace_on() {
eprintln!("HEAL {record}");
}
CROSSING_REPAIRS.with(|repairs| repairs.borrow_mut().push(record));
}
fn dump_flagged(solid: &BrepSolid, entry: &str) {
let Ok(directory) = std::env::var("BREP_HEAL_DUMP") else {
return;
};
if directory.is_empty() {
return;
}
let name: String = entry
.chars()
.map(|character| if character.is_alphanumeric() { character } else { '_' })
.collect();
let path = std::path::Path::new(&directory).join(format!("flagged-{name}.json"));
match serde_json::to_string(solid) {
Ok(text) => {
if let Err(error) = std::fs::write(&path, text) {
eprintln!("HEAL dump {}: {error}", path.display());
} else {
eprintln!("HEAL dumped the flagged body to {}", path.display());
}
}
Err(error) => eprintln!("HEAL dump: {error}"),
}
}
fn trace_on() -> bool {
std::env::var("BREP_HEAL_TRACE").is_ok_and(|value| !value.is_empty() && value != "0")
}
fn measured_fit_request(request: &mut crate::imprint::ImprintOptions) {
match std::env::var("BREP_HEAL_FIT_LANE").as_deref() {
Ok("local") => request.local_fit = true,
Ok("global") => request.local_fit = false,
_ => {}
}
if let Some(points) = std::env::var("BREP_HEAL_FIT_POINTS")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|points| *points >= 2)
{
request.maximum_fit_points = points;
}
if trace_on() {
eprintln!(
"HEAL imprint request: {} lane, {} stations, tolerance {:.3e}",
if request.local_fit { "local" } else { "global" },
request.maximum_fit_points,
request.tolerance
);
}
}
fn trace_solid(label: &str, solid: &BrepSolid) {
use rustc_hash::FxHashMap as HashMap;
let point: HashMap<u64, crate::Vec3> = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect();
eprintln!(
"HEAL {label}: {}F {}E {}V shells={}",
solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>(),
solid.edges.len(),
solid.vertices.len(),
solid.shells.len()
);
let mut uses: HashMap<u64, usize> = HashMap::default();
for shell in &solid.shells {
for face in &shell.faces {
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
*uses.entry(coedge.edge_id).or_default() += 1;
}
}
}
}
for edge in &solid.edges {
let start = point.get(&edge.start_vertex_id).copied().unwrap_or_default();
let end = point.get(&edge.end_vertex_id).copied().unwrap_or_default();
eprintln!(
"HEAL edge {:>3} uses={} v{}->v{} ({:.4},{:.4},{:.4})->({:.4},{:.4},{:.4}) deg={} '{}'",
edge.id,
uses.get(&edge.id).copied().unwrap_or(0),
edge.start_vertex_id,
edge.end_vertex_id,
start.x, start.y, start.z, end.x, end.y, end.z,
edge.degenerate,
edge.name.as_deref().unwrap_or("")
);
}
for shell in &solid.shells {
for face in &shell.faces {
eprintln!(
"HEAL face {:>3} loops={} '{}'",
face.id,
face.loops.len(),
face.name.as_deref().unwrap_or("")
);
for (index, loop_record) in face.loops.iter().enumerate() {
let text = loop_record
.coedges
.iter()
.map(|coedge| format!("{}{}", if coedge.forward { '+' } else { '-' }, coedge.edge_id))
.collect::<Vec<_>>()
.join(" ");
eprintln!("HEAL loop {index}: {text}");
}
}
}
}
fn trace_imprint(imprint: &crate::imprint::ImprintResultRecord) {
eprintln!(
"HEAL imprint: {} piece(s) {} edge_split(s) by_face={:?}",
imprint.pieces.len(),
imprint.edge_splits.len(),
imprint
.by_face
.iter()
.map(|face| (face.operand, face.face_id, face.piece_ids.clone()))
.collect::<Vec<_>>()
);
for piece in &imprint.pieces {
let start = piece.curve.evaluate(piece.t0).unwrap_or_default();
let end = piece.curve.evaluate(piece.t1).unwrap_or_default();
let middle = piece.curve.evaluate((piece.t0 + piece.t1) / 2.0).unwrap_or_default();
eprintln!(
"HEAL piece {} v{}->v{} ({:.4},{:.4},{:.4}) .. ({:.4},{:.4},{:.4}) .. ({:.4},{:.4},{:.4}) supports={:?} shared={:?}",
piece.id,
piece.start_vertex_id,
piece.end_vertex_id,
start.x, start.y, start.z,
middle.x, middle.y, middle.z,
end.x, end.y, end.z,
piece.support_faces.iter().map(|key| (key.operand, key.face_id)).collect::<Vec<_>>(),
piece.shared_edge,
);
}
for split in &imprint.edge_splits {
eprintln!(
"HEAL edge_split operand={} edge={} at {:?}",
split.operand, split.edge_id, split.parameters
);
}
for vertex in &imprint.vertices {
eprintln!(
"HEAL imprint vertex {} ({:.4},{:.4},{:.4})",
vertex.id, vertex.point.x, vertex.point.y, vertex.point.z
);
}
}
fn trace_fragments(fragments: &[crate::fragment::FaceFragmentRecord]) {
use crate::fragment::FragmentEdgeSource;
eprintln!("HEAL fragments: {}", fragments.len());
for (index, fragment) in fragments.iter().enumerate() {
eprintln!(
"HEAL fragment {index} face {} loops={} test=({:.4},{:.4},{:.4})",
fragment.source_face_id,
fragment.loops.len(),
fragment.test_point.x,
fragment.test_point.y,
fragment.test_point.z
);
for (loop_index, fragment_loop) in fragment.loops.iter().enumerate() {
let text = fragment_loop
.coedges
.iter()
.map(|coedge| {
let tag = match &coedge.source {
FragmentEdgeSource::Boundary { operand, edge_id } => format!("b{operand}:{edge_id}"),
FragmentEdgeSource::SharedBoundary { operand, edge_id } => format!("s{operand}:{edge_id}"),
FragmentEdgeSource::Imprint { piece_id } => format!("p{piece_id}"),
FragmentEdgeSource::Derived { start, end, .. } => format!(
"d({:.3},{:.3},{:.3})->({:.3},{:.3},{:.3})",
start.x, start.y, start.z, end.x, end.y, end.z
),
};
format!("{}{tag}", if coedge.forward { '+' } else { '-' })
})
.collect::<Vec<_>>()
.join(" ");
eprintln!("HEAL loop {loop_index}: {text}");
}
}
}