use super::face_split::FragmentIndex;
use super::loops::{edge_for, interior_points_in_polygons, pcurve_for, point_in_polygon, vertex_point};
use super::sampling::sample_chain;
use super::*;
use crate::tolerance::WELD_FLOOR;
use crate::{KernelRefusal, KernelStage, OrRefuse};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum LoopSplitMode {
Off,
Check,
On,
}
pub(super) fn loop_split_mode() -> LoopSplitMode {
match mode_switch!("BREP_LOOP_SPLIT", true) {
crate::env_read::Mode::Off => LoopSplitMode::Off,
crate::env_read::Mode::Check => LoopSplitMode::Check,
crate::env_read::Mode::On => LoopSplitMode::On,
}
}
#[derive(Clone, Debug, PartialEq)]
pub(super) enum Decline {
Cosurface,
SharedEdge,
DuplicatePiece,
OutOfDomain,
SeamHop,
EndNotOnLoop(u64),
EndTwiceUsed(u64),
RingTouches(u64),
CrossRegion(u64),
SameJunction(u64),
Crosses(u64),
RingTooSmall(u64),
RingOutside(u64),
NoInterior,
Incomplete,
}
impl std::fmt::Display for Decline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Clone)]
enum Side<'a> {
Boundary { coedge: &'a CoedgeRecord },
Cut { piece_id: u64, forward: bool, pcurve: &'a NurbsCurve, shared_ring: Option<(u8, u64, bool)> },
}
#[derive(Clone)]
struct Entry<'a> {
side: Side<'a>,
samples: Vec<Vec2>,
start_point: Vec3,
end_point: Vec3,
key: usize,
}
impl<'a> Entry<'a> {
fn start(&self) -> Vec2 {
self.samples[0]
}
fn end(&self) -> Vec2 {
self.samples[self.samples.len() - 1]
}
fn reversed(&self) -> Self {
let mut samples = self.samples.clone();
samples.reverse();
Entry {
side: match &self.side {
Side::Boundary { coedge } => Side::Boundary { coedge },
Side::Cut { piece_id, forward, pcurve, shared_ring } => Side::Cut { piece_id: *piece_id, forward: !forward, pcurve, shared_ring: *shared_ring },
},
samples,
start_point: self.end_point,
end_point: self.start_point,
key: self.key,
}
}
fn along_chain(&self) -> bool {
match &self.side {
Side::Boundary { .. } => true,
Side::Cut { forward, .. } => *forward,
}
}
}
#[derive(Clone)]
struct LoopUnderConstruction<'a> {
entries: Vec<Entry<'a>>,
}
impl<'a> LoopUnderConstruction<'a> {
fn min_key(&self) -> usize {
self.entries.iter().map(|entry| entry.key).min().unwrap_or(0)
}
fn rotate_like_arrangement(&mut self, reverse_loops: bool) {
let n = self.entries.len();
if n < 2 {
return;
}
let min_index = (0..n).min_by_key(|&k| self.entries[k].key).unwrap_or(0);
let start = if !reverse_loops && self.entries[min_index].along_chain() { min_index } else { (min_index + 1) % n };
self.entries.rotate_left(start);
}
fn polygon(&self) -> Vec<Vec2> {
let mut polygon = Vec::new();
for entry in &self.entries {
polygon.extend_from_slice(&entry.samples[..entry.samples.len() - 1]);
}
polygon
}
fn area(&self) -> f64 {
signed_area(&self.polygon())
}
fn arc(&self, from: usize, to: usize) -> Vec<Entry<'a>> {
let n = self.entries.len();
let mut out = Vec::new();
let mut index = from;
loop {
out.push(self.entries[index].clone());
index = (index + 1) % n;
if index == to {
break;
}
}
out
}
}
struct Region<'a> {
outer: LoopUnderConstruction<'a>,
holes: Vec<LoopUnderConstruction<'a>>,
}
fn insert_chord<'a>(
regions: &mut Vec<Region<'a>>,
ra: usize,
(la, ka): (usize, usize),
(lb, kb): (usize, usize),
forward: Vec<Entry<'a>>,
backward: Vec<Entry<'a>>,
outer_sign: bool,
) {
let region = regions.swap_remove(ra);
let Region { outer, mut holes } = region;
fn take<'b>(l: usize, outer: &LoopUnderConstruction<'b>, holes: &[LoopUnderConstruction<'b>]) -> LoopUnderConstruction<'b> {
if l == 0 { outer.clone() } else { holes[l - 1].clone() }
}
if la == lb {
let loop_a = take(la, &outer, &holes);
let mut first = loop_a.arc(ka, kb);
first.extend(backward);
let mut second = loop_a.arc(kb, ka);
second.extend(forward);
let first = LoopUnderConstruction { entries: first };
let second = LoopUnderConstruction { entries: second };
if la == 0 {
let first_polygon = first.polygon();
let mut first_holes = Vec::new();
let mut second_holes = Vec::new();
for hole in holes {
if point_in_polygon(hole.entries[0].start(), &first_polygon) {
first_holes.push(hole);
} else {
second_holes.push(hole);
}
}
regions.push(Region { outer: first, holes: first_holes });
regions.push(Region { outer: second, holes: second_holes });
} else {
holes.remove(la - 1);
let (hole_side, region_side) = if (first.area() >= 0.0) == outer_sign { (second, first) } else { (first, second) };
holes.push(hole_side);
regions.push(Region { outer, holes });
regions.push(Region { outer: region_side, holes: Vec::new() });
}
} else {
let loop_a = take(la, &outer, &holes);
let loop_b = take(lb, &outer, &holes);
let mut joined = loop_a.arc(ka, ka);
joined.extend(forward);
joined.extend(loop_b.arc(kb, kb));
joined.extend(backward);
let joined = LoopUnderConstruction { entries: joined };
let mut remaining: Vec<LoopUnderConstruction<'_>> = Vec::new();
for (l, hole) in holes.into_iter().enumerate() {
if l + 1 != la && l + 1 != lb {
remaining.push(hole);
}
}
if la == 0 || lb == 0 {
regions.push(Region { outer: joined, holes: remaining });
} else {
remaining.push(joined);
regions.push(Region { outer, holes: remaining });
}
}
}
fn signed_area(polygon: &[Vec2]) -> f64 {
let mut area = 0.0;
for index in 0..polygon.len() {
let a = polygon[index];
let b = polygon[(index + 1) % polygon.len()];
area += 0.5 * (crate::polygon::area_term([a.x, a.y], [b.x, b.y]));
}
area
}
fn uv(point: Vec3) -> Vec2 {
Vec2 { x: point.x, y: point.y }
}
fn cut_entry<'a>(piece_id: u64, pcurve: &'a NurbsCurve, samples: &[Vec2], start_point: Vec3, end_point: Vec3, key: usize) -> Entry<'a> {
Entry {
side: Side::Cut { piece_id, forward: true, pcurve, shared_ring: None },
samples: samples.to_vec(),
start_point,
end_point,
key,
}
}
fn chain_box(samples: &[Vec2], band: f64) -> [Vec2; 2] {
let mut low = Vec2 { x: f64::INFINITY, y: f64::INFINITY };
let mut high = Vec2 { x: f64::NEG_INFINITY, y: f64::NEG_INFINITY };
for point in samples {
low.x = low.x.min(point.x - band);
low.y = low.y.min(point.y - band);
high.x = high.x.max(point.x + band);
high.y = high.y.max(point.y + band);
}
[low, high]
}
fn boxes_meet(a: &[Vec2; 2], b: &[Vec2; 2]) -> bool {
a[0].x <= b[1].x && b[0].x <= a[1].x && a[0].y <= b[1].y && b[0].y <= a[1].y
}
fn chains_meet(piece: &[Vec2], other: &[Vec2], band: f64, clear: &[Vec2], clear_radius: f64) -> Option<Vec2> {
use super::sampling::point_segment_distance;
let is_clear = |point: Vec2| clear.iter().all(|centre| centre.sub(point).length() > clear_radius);
let other_segments: Vec<([Vec2; 2], [Vec2; 2])> =
other.windows(2).map(|pair| ([pair[0], pair[1]], chain_box(&pair[..], band))).collect();
for window in piece.windows(2) {
let (a, b) = (window[0], window[1]);
let segment_box = chain_box(window, band);
for (segment, other_box) in &other_segments {
if !boxes_meet(&segment_box, other_box) {
continue;
}
for point in [a, b] {
if point_segment_distance(point, segment[0], segment[1]) <= band && is_clear(point) {
return Some(point);
}
}
for point in *segment {
if point_segment_distance(point, a, b) <= band && is_clear(point) {
return Some(point);
}
}
if let Some([t, _]) = crate::arrangement::segment_intersection(a, b, segment[0], segment[1], band) {
let hit = a.add(b.sub(a).scale(t));
if is_clear(hit) {
return Some(hit);
}
}
}
}
None
}
pub(super) fn loop_split_face(
solid: &BrepSolid,
operand: u8,
face: &FaceRecord,
face_key: FaceKey,
piece_ids: &[u64],
index: &FragmentIndex,
) -> Result<Result<Vec<FaceFragmentRecord>, Decline>, KernelRefusal> {
let cycles = !matches!(std::env::var("BREP_LOOP_SPLIT_CYCLES").as_deref(), Ok("0") | Ok("off"));
let shared_rings = !matches!(std::env::var("BREP_LOOP_SPLIT_SHARED_RINGS").as_deref(), Ok("0") | Ok("off"));
let touch_rings = touch_rings_enabled();
loop_split_face_impl(solid, operand, face, face_key, piece_ids, index, cycles, shared_rings, touch_rings, touch_rings && junction_chords_enabled())
}
pub(super) fn periodic_chart_joins_enabled() -> bool {
switch!(live "BREP_LOOP_SPLIT_PERIODIC_JOINS", true)
}
pub(super) fn junction_chords_enabled() -> bool {
switch!(live "BREP_LOOP_SPLIT_JUNCTION_CHORDS", true)
}
pub(super) fn touch_rings_enabled() -> bool {
switch!(live "BREP_LOOP_SPLIT_TOUCH_RINGS", true)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn loop_split_face_with_cycles(
solid: &BrepSolid,
operand: u8,
face: &FaceRecord,
face_key: FaceKey,
piece_ids: &[u64],
index: &FragmentIndex,
cycles: bool,
) -> Result<Result<Vec<FaceFragmentRecord>, Decline>, KernelRefusal> {
loop_split_face_with_options(solid, operand, face, face_key, piece_ids, index, cycles, false)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn loop_split_face_with_options(
solid: &BrepSolid,
operand: u8,
face: &FaceRecord,
face_key: FaceKey,
piece_ids: &[u64],
index: &FragmentIndex,
cycles: bool,
shared_rings: bool,
) -> Result<Result<Vec<FaceFragmentRecord>, Decline>, KernelRefusal> {
loop_split_face_impl(solid, operand, face, face_key, piece_ids, index, cycles, shared_rings, false, false)
}
#[allow(clippy::too_many_arguments)]
fn loop_split_face_impl(
solid: &BrepSolid,
operand: u8,
face: &FaceRecord,
face_key: FaceKey,
piece_ids: &[u64],
index: &FragmentIndex,
cycles: bool,
shared_rings: bool,
touch_rings: bool,
junction_chords: bool,
) -> Result<Result<Vec<FaceFragmentRecord>, Decline>, KernelRefusal> {
if index.cosurface_pairs.iter().any(|(a, b)| *a == face_key || *b == face_key) {
return Ok(Err(Decline::Cosurface));
}
let u_domain = KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u).or_refuse(KernelStage::Fragment, "new")?.domain();
let v_domain = KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v).or_refuse(KernelStage::Fragment, "new")?.domain();
let diagonal = ((u_domain[1] - u_domain[0]).powi(2) + (v_domain[1] - v_domain[0]).powi(2)).sqrt();
let arrangement_tolerance = 1e-7f64.max(diagonal * 1e-6);
let uv_match = (arrangement_tolerance * 100.0).max(diagonal * 1e-5);
let domain_slack = arrangement_tolerance * 100.0;
let in_domain = |samples: &[Vec2]| {
samples.iter().all(|point| {
point.x >= u_domain[0] - domain_slack
&& point.x <= u_domain[1] + domain_slack
&& point.y >= v_domain[0] - domain_slack
&& point.y <= v_domain[1] + domain_slack
})
};
let vertex_band = WELD_FLOOR;
let mut loops: Vec<LoopUnderConstruction<'_>> = Vec::with_capacity(face.loops.len());
let mut next_key = 0usize;
for loop_record in &face.loops {
let mut entries = Vec::with_capacity(loop_record.coedges.len());
for coedge in &loop_record.coedges {
let key = next_key;
next_key += 1;
let edge = edge_for(solid, coedge.edge_id)?;
let (start_vertex, end_vertex) = if coedge.forward {
(edge.start_vertex_id, edge.end_vertex_id)
} else {
(edge.end_vertex_id, edge.start_vertex_id)
};
let samples = sample_chain(&coedge.pcurve)?;
if samples.len() < 2 {
return Ok(Err(Decline::Incomplete));
}
if !in_domain(&samples) {
return Ok(Err(Decline::OutOfDomain));
}
entries.push(Entry {
side: Side::Boundary { coedge },
samples,
start_point: vertex_point(solid, start_vertex)?,
end_point: vertex_point(solid, end_vertex)?,
key,
});
}
if entries.is_empty() {
return Ok(Err(Decline::Incomplete));
}
for k in 0..entries.len() {
let next = (k + 1) % entries.len();
if entries[k].end().sub(entries[next].start()).length() > uv_match {
return Ok(Err(Decline::SeamHop));
}
}
loops.push(LoopUnderConstruction { entries });
}
if loops.is_empty() {
return Ok(Err(Decline::Incomplete));
}
let areas: Vec<f64> = loops.iter().map(|l| l.area()).collect();
let outer_index = (0..loops.len()).max_by(|a, b| areas[*a].abs().total_cmp(&areas[*b].abs())).unwrap_or(0);
let outer_sign = areas[outer_index] >= 0.0;
let closed = face.surface.closed_directions().or_refuse(KernelStage::Fragment, "closed_directions")?;
let reverse_loops = if closed.0 || closed.1 { !face.same_sense } else { !outer_sign };
let outer = loops.swap_remove(outer_index);
let mut regions = vec![Region { outer, holes: loops }];
let boundary_keys = next_key;
struct OpenPiece<'a> {
id: u64,
pcurve: &'a NurbsCurve,
samples: Vec<Vec2>,
start_point: Vec3,
end_point: Vec3,
key: usize,
}
struct Ring<'a> {
id: u64,
entries: Vec<Entry<'a>>,
samples: Vec<Vec2>,
point: Vec3,
area: f64,
}
let mut interior_arcs = Vec::new();
let mut touch_arcs: Vec<(Entry<'_>, u64, u64)> = Vec::new();
let mut open_pieces: Vec<OpenPiece<'_>> = Vec::new();
let mut rings: Vec<Ring<'_>> = Vec::new();
let mut keys: HashSet<((i64, i64), (i64, i64), (i64, i64))> = HashSet::default();
let quantize = |point: Vec2| ((point.x / arrangement_tolerance).round() as i64, (point.y / arrangement_tolerance).round() as i64);
let on_a_loop = |point: Vec3, regions: &[Region<'_>]| {
regions.iter().any(|region| {
std::iter::once(®ion.outer)
.chain(region.holes.iter())
.any(|l| l.entries.iter().any(|entry| entry.start_point.sub(point).length() <= vertex_band))
})
};
for (position, piece_id) in piece_ids.iter().enumerate() {
let key = boundary_keys + position;
let Some(piece) = index.pieces_by_id.get(piece_id) else {
return Ok(Err(Decline::Incomplete));
};
let shared_ring = if shared_rings && piece_ids.len() == 1
&& face.surface.is_affine().unwrap_or(false)
&& piece.start_vertex_id == piece.end_vertex_id
&& piece.curve.evaluate(piece.t0).ok().zip(piece.curve.evaluate(piece.t1).ok())
.is_some_and(|(a, b)| a.sub(b).length() <= 1e-6)
{
piece.shared_edge
} else {
None
};
if piece.shared_edge.is_some() && shared_ring.is_none() {
return Ok(Err(Decline::SharedEdge));
}
let pcurve = pcurve_for(piece, face_key)?;
let samples = sample_chain(pcurve)?;
if samples.len() < 2 {
return Ok(Err(Decline::Incomplete));
}
if !in_domain(&samples) {
return Ok(Err(Decline::OutOfDomain));
}
let [t0, t1] = pcurve.domain().or_refuse(KernelStage::Fragment, "domain")?;
let middle = uv(pcurve.evaluate(0.5 * (t0 + t1)).or_refuse(KernelStage::Fragment, "evaluate")?);
let mut ends = [quantize(samples[0]), quantize(samples[samples.len() - 1])];
ends.sort();
if !keys.insert((ends[0], quantize(middle), ends[1])) {
return Ok(Err(Decline::DuplicatePiece));
}
let (Some(&start_point), Some(&end_point)) =
(index.vertex_points.get(&piece.start_vertex_id), index.vertex_points.get(&piece.end_vertex_id))
else {
return Ok(Err(Decline::Incomplete));
};
let closed = piece.start_vertex_id == piece.end_vertex_id
&& samples[0].sub(samples[samples.len() - 1]).length() <= uv_match;
if std::env::var_os("BREP_DEBUG_LOOP_SPLIT").is_some() {
let nearest = |point: Vec3, at: Vec2| {
regions
.iter()
.flat_map(|region| std::iter::once(®ion.outer).chain(region.holes.iter()))
.flat_map(|l| l.entries.iter())
.map(|entry| (entry.start_point.sub(point).length(), entry.start().sub(at).length()))
.min_by(|a, b| a.0.total_cmp(&b.0))
.unwrap_or((f64::NAN, f64::NAN))
};
let (ds3, dsuv) = nearest(start_point, samples[0]);
let (de3, deuv) = nearest(end_point, samples[samples.len() - 1]);
eprintln!(
"loop-split face {} piece {} vertices {}->{} closed={closed} uv_start=({:.6},{:.6}) uv_end=({:.6},{:.6}) nearest_junction start 3d={ds3:.3e} uv={dsuv:.3e} end 3d={de3:.3e} uv={deuv:.3e} uv_match={uv_match:.3e} domain u={u_domain:?} v={v_domain:?}",
face.id, piece_id, piece.start_vertex_id, piece.end_vertex_id, samples[0].x, samples[0].y, samples[samples.len() - 1].x, samples[samples.len() - 1].y
);
}
if closed {
if touch_rings && shared_ring.is_none() && on_a_loop(start_point, ®ions) {
touch_arcs.push((cut_entry(*piece_id, pcurve, &samples, start_point, end_point, key), piece.start_vertex_id, piece.end_vertex_id));
continue;
}
if on_a_loop(start_point, ®ions) {
return Ok(Err(Decline::RingTouches(*piece_id)));
}
let area = signed_area(&samples[..samples.len() - 1]);
if area.abs() <= (arrangement_tolerance * arrangement_tolerance).max(1e-24) {
return Ok(Err(Decline::RingTooSmall(*piece_id)));
}
let mut entry = cut_entry(*piece_id, pcurve, &samples, start_point, end_point, key);
if let Side::Cut { shared_ring: target, .. } = &mut entry.side {
*target = shared_ring;
}
rings.push(Ring { id: *piece_id, entries: vec![entry], samples, point: start_point, area });
} else {
if !on_a_loop(start_point, ®ions) || !on_a_loop(end_point, ®ions) {
if cycles && piece_ids.len() == 2 && face.surface.is_affine().unwrap_or(false)
&& !on_a_loop(start_point, ®ions) && !on_a_loop(end_point, ®ions)
{
interior_arcs.push(cut_entry(*piece_id, pcurve, &samples, start_point, end_point, key));
continue;
}
if touch_rings {
touch_arcs.push((cut_entry(*piece_id, pcurve, &samples, start_point, end_point, key), piece.start_vertex_id, piece.end_vertex_id));
continue;
}
return Ok(Err(Decline::EndNotOnLoop(*piece_id)));
}
open_pieces.push(OpenPiece { id: *piece_id, pcurve, samples, start_point, end_point, key });
}
}
if !interior_arcs.is_empty() {
let first_id = piece_ids[0];
if interior_arcs.len() != 2 || !open_pieces.is_empty() || !rings.is_empty() {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
}
let first = interior_arcs.remove(0);
let second = interior_arcs.remove(0);
let a = index.pieces_by_id[&piece_ids[0]];
let b = index.pieces_by_id[&piece_ids[1]];
let second = if a.end_vertex_id == b.start_vertex_id && a.start_vertex_id == b.end_vertex_id {
second
} else if a.end_vertex_id == b.end_vertex_id && a.start_vertex_id == b.start_vertex_id {
second.reversed()
} else {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
};
if first.end().sub(second.start()).length() > uv_match
|| second.end().sub(first.start()).length() > uv_match
{
return Ok(Err(Decline::SeamHop));
}
let clear_radius = (arrangement_tolerance * 50.0).max(diagonal * 1.5e-3);
if chains_meet(&first.samples, &second.samples, uv_match,
&[first.start(), first.end()], clear_radius).is_some()
{
return Ok(Err(Decline::Crosses(first_id)));
}
let mut samples = first.samples[..first.samples.len() - 1].to_vec();
samples.extend_from_slice(&second.samples);
let area = signed_area(&samples[..samples.len() - 1]);
if area.abs() <= (arrangement_tolerance * arrangement_tolerance).max(1e-24) {
return Ok(Err(Decline::RingTooSmall(first_id)));
}
let point = first.start_point;
rings.push(Ring { id: first_id, entries: vec![first, second], samples, point, area });
}
let mut chord_arcs: Vec<(Entry<'_>, u64, u64)> = Vec::new();
if junction_chords && !touch_arcs.is_empty() && open_pieces.is_empty() && rings.is_empty() && interior_arcs.is_empty() {
let mut junctions: Vec<u64> = Vec::new();
for (entry, start_id, end_id) in &touch_arcs {
for (id, point) in [(*start_id, entry.start_point), (*end_id, entry.end_point)] {
if on_a_loop(point, ®ions) && !junctions.contains(&id) {
junctions.push(id);
}
}
}
let periodic_chord = junctions.len() == 1 && touch_arcs.len() == 2
&& (closed.0 != closed.1) && periodic_chart_joins_enabled()
&& {
let junction = junctions[0];
let ends = touch_arcs.iter().map(|(entry, start, end)| {
if *start == junction && *end != junction {
Some((entry.start(), entry.start_point, *end, entry.end()))
} else if *end == junction && *start != junction {
Some((entry.end(), entry.end_point, *start, entry.start()))
} else { None }
}).collect::<Option<Vec<_>>>();
ends.is_some_and(|ends| {
let (a, pa, ia, qa) = ends[0];
let (b, pb, ib, qb) = ends[1];
let (a_periodic, b_periodic, a_other, b_other, domain) = if closed.0 {
(a.x, b.x, a.y, b.y, u_domain)
} else { (a.y, b.y, a.x, b.x, v_domain) };
ia == ib && qa.sub(qb).length() <= uv_match
&& pa.sub(pb).length() <= vertex_band
&& (a_other - b_other).abs() <= uv_match
&& (((a_periodic - domain[0]).abs() <= uv_match && (b_periodic - domain[1]).abs() <= uv_match)
|| ((b_periodic - domain[0]).abs() <= uv_match && (a_periodic - domain[1]).abs() <= uv_match))
})
};
if junctions.len() >= 2 || periodic_chord {
chord_arcs = std::mem::take(&mut touch_arcs);
}
}
let mut touch: Option<Ring<'_>> = None;
if !touch_arcs.is_empty() {
let first_id = match &touch_arcs[0].0.side { Side::Cut { piece_id, .. } => *piece_id, Side::Boundary { .. } => 0 };
if !open_pieces.is_empty() || !rings.is_empty() || !interior_arcs.is_empty() {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
}
let mut junctions: Vec<u64> = Vec::new();
for (entry, start_id, end_id) in &touch_arcs {
for (id, point) in [(*start_id, entry.start_point), (*end_id, entry.end_point)] {
if on_a_loop(point, ®ions) && !junctions.contains(&id) {
junctions.push(id);
}
}
}
if junctions.len() != 1 {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
}
let junction = junctions[0];
let mut uses: HashMap<u64, usize> = HashMap::default();
for (_, start_id, end_id) in &touch_arcs {
*uses.entry(*start_id).or_default() += 1;
*uses.entry(*end_id).or_default() += 1;
}
if uses.values().any(|&count| count != 2) {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
}
let mut remaining: Vec<(Entry<'_>, u64, u64)> = std::mem::take(&mut touch_arcs);
let mut entries: Vec<Entry<'_>> = Vec::with_capacity(remaining.len());
let mut at = junction;
while !remaining.is_empty() {
let Some(position) = remaining.iter().position(|(_, start_id, end_id)| *start_id == at || *end_id == at) else {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
};
let (entry, start_id, end_id) = remaining.remove(position);
if start_id == at {
at = end_id;
entries.push(entry);
} else {
at = start_id;
entries.push(entry.reversed());
}
if at == junction && !remaining.is_empty() {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
}
}
if at != junction {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
}
for k in 0..entries.len() {
let next = (k + 1) % entries.len();
if entries[k].end().sub(entries[next].start()).length() > uv_match {
return Ok(Err(Decline::SeamHop));
}
}
let mut samples: Vec<Vec2> = Vec::new();
for entry in &entries {
samples.extend_from_slice(&entry.samples[..entry.samples.len() - 1]);
}
samples.push(entries[0].start());
let area = signed_area(&samples[..samples.len() - 1]);
if area.abs() <= (arrangement_tolerance * arrangement_tolerance).max(1e-24) {
return Ok(Err(Decline::RingTooSmall(first_id)));
}
let point = entries[0].start_point;
touch = Some(Ring { id: first_id, entries, samples, point, area });
}
{
let clear_radius = (arrangement_tolerance * 50.0).max(diagonal * 1.5e-3);
let band = uv_match;
let loop_chains: Vec<&[Vec2]> = regions
.iter()
.flat_map(|region| std::iter::once(®ion.outer).chain(region.holes.iter()))
.flat_map(|l| l.entries.iter().map(|entry| entry.samples.as_slice()))
.collect();
let loop_boxes: Vec<[Vec2; 2]> = loop_chains.iter().map(|chain| chain_box(chain, band)).collect();
let piece_chains: Vec<(u64, &[Vec2], Vec<Vec2>)> = open_pieces
.iter()
.map(|piece| (piece.id, piece.samples.as_slice(), vec![piece.samples[0], piece.samples[piece.samples.len() - 1]]))
.chain(rings.iter().map(|ring| (ring.id, ring.samples.as_slice(), Vec::new())))
.chain(touch.iter().map(|ring| (ring.id, ring.samples.as_slice(), vec![ring.samples[0]])))
.chain(chord_arcs.iter().map(|(entry, _, _)| {
let id = match &entry.side { Side::Cut { piece_id, .. } => *piece_id, Side::Boundary { .. } => 0 };
let on_loop_ends = [(entry.start_point, entry.start()), (entry.end_point, entry.end())]
.into_iter()
.filter(|(point, _)| on_a_loop(*point, ®ions))
.map(|(_, at)| at)
.collect();
(id, entry.samples.as_slice(), on_loop_ends)
}))
.collect();
let chord_start = piece_chains.len() - chord_arcs.len();
let chord_ends: Vec<Vec<Vec2>> = chord_arcs.iter().map(|(entry, _, _)| vec![entry.start(), entry.end()]).collect();
let piece_boxes: Vec<[Vec2; 2]> = piece_chains.iter().map(|(_, chain, _)| chain_box(chain, band)).collect();
for (i, (id, chain, clear)) in piece_chains.iter().enumerate() {
for (loop_chain, loop_box) in loop_chains.iter().zip(&loop_boxes) {
if boxes_meet(&piece_boxes[i], loop_box) && chains_meet(chain, loop_chain, band, clear, clear_radius).is_some() {
return Ok(Err(Decline::Crosses(*id)));
}
}
for (j, (_, other, _)) in piece_chains.iter().enumerate().skip(i + 1) {
let shared: Vec<Vec2> = if i >= chord_start {
let (mine, theirs) = (&chord_ends[i - chord_start], &chord_ends[j - chord_start]);
mine.iter().copied().filter(|end| theirs.iter().any(|o| o.sub(*end).length() <= uv_match)).collect()
} else {
Vec::new()
};
let radius = if shared.is_empty() { 0.0 } else { clear_radius };
if boxes_meet(&piece_boxes[i], &piece_boxes[j]) && chains_meet(chain, other, band, &shared, radius).is_some() {
return Ok(Err(Decline::Crosses(*id)));
}
}
}
}
for ring in &rings {
let shared = open_pieces
.iter()
.any(|piece| piece.start_point.sub(ring.point).length() <= vertex_band || piece.end_point.sub(ring.point).length() <= vertex_band)
|| rings.iter().filter(|other| other.id != ring.id).any(|other| other.point.sub(ring.point).length() <= vertex_band);
if shared {
return Ok(Err(Decline::RingTouches(ring.id)));
}
}
for (i, piece) in open_pieces.iter().enumerate() {
for end in [(piece.start_point, piece.samples[0]), (piece.end_point, piece.samples[piece.samples.len() - 1])] {
let twice = open_pieces.iter().enumerate().any(|(j, other)| {
let others = if i == j {
vec![]
} else {
vec![(other.start_point, other.samples[0]), (other.end_point, other.samples[other.samples.len() - 1])]
};
others.iter().any(|(p, q)| p.sub(end.0).length() <= vertex_band && q.sub(end.1).length() <= uv_match)
});
if twice {
return Ok(Err(Decline::EndTwiceUsed(piece.id)));
}
}
}
for piece in &open_pieces {
let locate = |point: Vec3, at: Vec2, regions: &[Region<'_>]| -> Option<(usize, usize, usize)> {
let mut best: Option<((usize, usize, usize), f64)> = None;
for (r, region) in regions.iter().enumerate() {
for (l, l_record) in std::iter::once(®ion.outer).chain(region.holes.iter()).enumerate() {
for (k, entry) in l_record.entries.iter().enumerate() {
if entry.start_point.sub(point).length() > vertex_band {
continue;
}
let distance = entry.start().sub(at).length();
if distance <= uv_match && best.is_none_or(|(_, d)| distance < d) {
best = Some(((r, l, k), distance));
}
}
}
}
best.map(|(found, _)| found)
};
let Some((ra, la, ka)) = locate(piece.start_point, piece.samples[0], ®ions) else {
return Ok(Err(Decline::EndNotOnLoop(piece.id)));
};
let Some((rb, lb, kb)) = locate(piece.end_point, piece.samples[piece.samples.len() - 1], ®ions) else {
return Ok(Err(Decline::EndNotOnLoop(piece.id)));
};
if ra != rb {
return Ok(Err(Decline::CrossRegion(piece.id)));
}
if la == lb && ka == kb {
return Ok(Err(Decline::SameJunction(piece.id)));
}
let forward = cut_entry(piece.id, piece.pcurve, &piece.samples, piece.start_point, piece.end_point, piece.key);
let backward = forward.reversed();
insert_chord(&mut regions, ra, (la, ka), (lb, kb), vec![forward], vec![backward], outer_sign);
}
let mut remaining = chord_arcs;
while !remaining.is_empty() {
let first_id = match &remaining[0].0.side { Side::Cut { piece_id, .. } => *piece_id, Side::Boundary { .. } => 0 };
let mut path: Option<Vec<(usize, bool)>> = None;
'search: for start in 0..remaining.len() {
for forward in [true, false] {
let (entry, start_id, end_id) = &remaining[start];
let (from_point, from_id, mut at_id, mut at_point) = if forward {
(entry.start_point, *start_id, *end_id, entry.end_point)
} else {
(entry.end_point, *end_id, *start_id, entry.start_point)
};
if !on_a_loop(from_point, ®ions) {
continue;
}
let mut steps = vec![(start, forward)];
let mut visited = vec![from_id];
loop {
if on_a_loop(at_point, ®ions) {
path = Some(steps);
break 'search;
}
if visited.contains(&at_id) {
break;
}
visited.push(at_id);
let next = remaining.iter().enumerate().find(|(k, (_, s, e))| {
!steps.iter().any(|(used, _)| used == k) && (*s == at_id || *e == at_id)
});
let Some((k, (next_entry, s, e))) = next else {
break;
};
let along = *s == at_id;
steps.push((k, along));
(at_id, at_point) = if along { (*e, next_entry.end_point) } else { (*s, next_entry.start_point) };
}
}
}
let Some(steps) = path else {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
};
let forward: Vec<Entry<'_>> = steps.iter().map(|(k, along)| if *along { remaining[*k].0.clone() } else { remaining[*k].0.reversed() }).collect();
let backward: Vec<Entry<'_>> = forward.iter().rev().map(Entry::reversed).collect();
let mut used: Vec<usize> = steps.iter().map(|(k, _)| *k).collect();
used.sort_unstable_by(|a, b| b.cmp(a));
for k in used {
remaining.remove(k);
}
let first_samples = &forward[0].samples;
let middle = first_samples.len() / 2;
let probe = first_samples[middle - 1].add(first_samples[middle]).scale(0.5);
let Some(r) = regions.iter().position(|region| {
point_in_polygon(probe, ®ion.outer.polygon()) && !region.holes.iter().any(|hole| point_in_polygon(probe, &hole.polygon()))
}) else {
return Ok(Err(Decline::RingOutside(first_id)));
};
let locate = |point: Vec3, at: Vec2| -> Option<(usize, usize)> {
let mut found = None;
let region = ®ions[r];
for (l, l_record) in std::iter::once(®ion.outer).chain(region.holes.iter()).enumerate() {
for (k, entry) in l_record.entries.iter().enumerate() {
if entry.start_point.sub(point).length() <= vertex_band && entry.start().sub(at).length() <= uv_match {
if found.is_some() {
return None;
}
found = Some((l, k));
}
}
}
found
};
let last = &forward[forward.len() - 1];
let (Some((la, ka)), Some((lb, kb))) = (locate(forward[0].start_point, forward[0].start()), locate(last.end_point, last.end())) else {
return Ok(Err(Decline::EndNotOnLoop(first_id)));
};
if la == lb && ka == kb {
return Ok(Err(Decline::SameJunction(first_id)));
}
insert_chord(&mut regions, r, (la, ka), (lb, kb), forward, backward, outer_sign);
}
if let Some(ring) = &touch {
let mut found: Option<(usize, usize, usize)> = None;
let mut best = f64::INFINITY;
for (r, region) in regions.iter().enumerate() {
for (l, l_record) in std::iter::once(®ion.outer).chain(region.holes.iter()).enumerate() {
for (k, entry) in l_record.entries.iter().enumerate() {
if entry.start_point.sub(ring.point).length() > vertex_band {
continue;
}
let distance = entry.start().sub(ring.samples[0]).length();
if distance <= uv_match && distance < best {
best = distance;
found = Some((r, l, k));
}
}
}
}
let Some((r, l, k)) = found else {
return Ok(Err(Decline::EndNotOnLoop(ring.id)));
};
let probe = ring.samples[ring.samples.len() / 2];
let region = ®ions[r];
let inside_outer = point_in_polygon(probe, ®ion.outer.polygon());
let in_a_hole = region.holes.iter().any(|hole| point_in_polygon(probe, &hole.polygon()));
if !inside_outer || in_a_hole {
return Ok(Err(Decline::RingOutside(ring.id)));
}
let along = LoopUnderConstruction { entries: ring.entries.clone() };
let against = LoopUnderConstruction { entries: ring.entries.iter().rev().map(Entry::reversed).collect() };
let (as_outer, as_hole) = if (ring.area >= 0.0) == outer_sign { (along, against) } else { (against, along) };
let Region { outer, holes } = regions.swap_remove(r);
let ring_polygon = as_outer.polygon();
let host = if l == 0 { outer.clone() } else { holes[l - 1].clone() };
let mut pinched = host.arc(k, k);
pinched.extend(as_hole.entries);
let pinched = LoopUnderConstruction { entries: pinched };
let (mut outer, holes) = (outer, holes);
let mut inside_ring = Vec::new();
let mut outside_ring = Vec::new();
for (index, hole) in holes.into_iter().enumerate() {
if index + 1 == l {
outside_ring.push(pinched.clone());
} else if point_in_polygon(hole.entries[0].start(), &ring_polygon) {
inside_ring.push(hole);
} else {
outside_ring.push(hole);
}
}
if l == 0 {
outer = pinched;
}
regions.push(Region { outer, holes: outside_ring });
regions.push(Region { outer: as_outer, holes: inside_ring });
}
rings.sort_by(|a, b| b.area.abs().total_cmp(&a.area.abs()));
for ring in &rings {
let probe = ring.samples[0];
let containing = regions.iter().position(|region| {
point_in_polygon(probe, ®ion.outer.polygon()) && !region.holes.iter().any(|hole| point_in_polygon(probe, &hole.polygon()))
});
let Some(r) = containing else {
return Ok(Err(Decline::RingOutside(ring.id)));
};
let along = LoopUnderConstruction { entries: ring.entries.clone() };
let against = LoopUnderConstruction { entries: ring.entries.iter().rev().map(Entry::reversed).collect() };
let (as_outer, as_hole) = if (ring.area >= 0.0) == outer_sign {
(along, against)
} else {
(against, along)
};
let ring_polygon = as_outer.polygon();
let mut inside_ring = Vec::new();
let mut outside_ring = Vec::new();
for hole in std::mem::take(&mut regions[r].holes) {
if point_in_polygon(hole.entries[0].start(), &ring_polygon) {
inside_ring.push(hole);
} else {
outside_ring.push(hole);
}
}
outside_ring.push(as_hole);
regions[r].holes = outside_ring;
regions.push(Region { outer: as_outer, holes: inside_ring });
}
let mut ordered: Vec<(f64, Region<'_>)> = regions.into_iter().map(|region| (region.outer.area().abs(), region)).collect();
ordered.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut fragments = Vec::with_capacity(ordered.len());
for (outer_area, mut region) in ordered {
let loop_key = |l: &LoopUnderConstruction<'_>| {
let mut keys: Vec<(u64, u64)> = l.entries.iter().map(|entry| (entry.key as u64, 0)).collect();
keys.sort_unstable();
keys
};
let mut hole_keys: Vec<Vec<(u64, u64)>> = region.holes.iter().map(loop_key).collect();
hole_keys.sort_unstable();
let order = FragmentOrder { area: outer_area, key: (loop_key(®ion.outer), hole_keys) };
region.holes.sort_by_key(|hole| hole.min_key());
region.outer.rotate_like_arrangement(reverse_loops);
for hole in &mut region.holes {
hole.rotate_like_arrangement(reverse_loops);
}
let mut outer_polygon = region.outer.polygon();
let mut hole_polygons: Vec<Vec<Vec2>> = region.holes.iter().map(|hole| hole.polygon()).collect();
if signed_area(&outer_polygon) < 0.0 {
outer_polygon.reverse();
for hole in &mut hole_polygons {
hole.reverse();
}
}
let area = region.outer.area() + region.holes.iter().map(|hole| hole.area()).sum::<f64>();
let candidates = interior_points_in_polygons(&outer_polygon, &hole_polygons, area, 3);
let debug_no_interior = |candidates: &[Vec2], class: Option<PolygonClass>| {
if std::env::var_os("BREP_DEBUG_LOOP_SPLIT").is_some() {
eprintln!(
"loop-split face {} NoInterior: outer area {:.4e} ({} points) holes {:?} area {area:.4e} candidates {:?} class {class:?} outer_sign {outer_sign}",
face.id,
signed_area(&outer_polygon),
outer_polygon.len(),
hole_polygons.iter().map(|h| format!("{:.4e}/{}", signed_area(h), h.len())).collect::<Vec<_>>(),
candidates.iter().map(|c| format!("({:.4},{:.4})", c.x, c.y)).collect::<Vec<_>>(),
);
}
};
let Some(&test_uv) = candidates.first() else {
debug_no_interior(&candidates, None);
return Ok(Err(Decline::NoInterior));
};
let class = parameter_point_in_face(face, test_uv, 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")?;
if class != PolygonClass::Inside {
debug_no_interior(&candidates, Some(class));
return Ok(Err(Decline::NoInterior));
}
let mut extra_test_points = Vec::new();
for &candidate in candidates.iter().skip(1) {
if parameter_point_in_face(face, candidate, 1e-9).or_refuse(KernelStage::Fragment, "parameter_point_in_face")? == PolygonClass::Inside {
extra_test_points.push(face.surface.evaluate(candidate.x, candidate.y).or_refuse(KernelStage::Fragment, "evaluate")?);
}
}
let mut record_loops = Vec::with_capacity(1 + region.holes.len());
for l in std::iter::once(®ion.outer).chain(region.holes.iter()) {
let mut coedges = Vec::with_capacity(l.entries.len());
for entry in &l.entries {
coedges.push(match &entry.side {
Side::Boundary { coedge } => FragmentCoedge {
source: FragmentEdgeSource::Boundary { operand, edge_id: coedge.edge_id },
forward: coedge.forward,
pcurve: coedge.pcurve.clone(),
},
Side::Cut { piece_id, forward, pcurve, shared_ring } => FragmentCoedge {
source: match shared_ring {
Some((operand, edge_id, _)) => FragmentEdgeSource::SharedBoundary { operand: *operand, edge_id: *edge_id },
None => FragmentEdgeSource::Imprint { piece_id: *piece_id },
},
forward: shared_ring.map_or(*forward, |(_, _, aligned)| *forward == aligned),
pcurve: if *forward { (*pcurve).clone() } else { pcurve.reversed().or_refuse(KernelStage::Fragment, "reversed")? },
},
});
}
record_loops.push(FragmentLoop { coedges });
}
fragments.push(FaceFragmentRecord {
operand,
source_face_id: face.id,
surface: face.surface.clone(),
same_sense: face.same_sense,
loops: record_loops,
test_point: face.surface.evaluate(test_uv.x, test_uv.y).or_refuse(KernelStage::Fragment, "evaluate")?,
test_uv,
extra_test_points,
order: Some(order),
});
}
Ok(Ok(fragments))
}
fn coedge_key(coedge: &FragmentCoedge) -> String {
let source = 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!("I{piece_id}"),
FragmentEdgeSource::Derived { .. } => "D".to_string(),
};
format!("{source}/{}/{}", coedge.forward, serde_json::to_string(&coedge.pcurve).unwrap_or_default())
}
fn loop_key(loop_record: &FragmentLoop) -> String {
let keys: Vec<String> = loop_record.coedges.iter().map(coedge_key).collect();
let n = keys.len();
(0..n)
.map(|start| (0..n).map(|k| keys[(start + k) % n].as_str()).collect::<Vec<_>>().join("|"))
.min()
.unwrap_or_default()
}
fn fragment_key(fragment: &FaceFragmentRecord) -> String {
let mut loops: Vec<String> = fragment.loops.iter().map(loop_key).collect();
loops.sort();
format!("{}/{}", fragment.same_sense, loops.join("||"))
}
fn record_polygons(fragment: &FaceFragmentRecord) -> Result<(Vec<Vec2>, Vec<Vec<Vec2>>), KernelRefusal> {
let mut polygons = Vec::with_capacity(fragment.loops.len());
for loop_record in &fragment.loops {
let mut polygon = Vec::new();
for coedge in &loop_record.coedges {
let samples = sample_chain(&coedge.pcurve)?;
polygon.extend_from_slice(&samples[..samples.len().saturating_sub(1)]);
}
polygons.push(polygon);
}
let outer = (0..polygons.len())
.max_by(|a, b| signed_area(&polygons[*a]).abs().total_cmp(&signed_area(&polygons[*b]).abs()))
.unwrap_or(0);
let outer_polygon = polygons.swap_remove(outer);
Ok((outer_polygon, polygons))
}
fn record_contains(fragment: &FaceFragmentRecord, point: Vec2) -> Result<bool, KernelRefusal> {
let (outer, holes) = record_polygons(fragment)?;
Ok(point_in_polygon(point, &outer) && !holes.iter().any(|hole| point_in_polygon(point, hole)))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Agreement {
pub(super) fragments: usize,
pub(super) same_order: usize,
pub(super) same_test_uv: usize,
}
impl std::fmt::Display for Agreement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"agree {} fragments ({} in the arrangement's loop order, {} with its test point)",
self.fragments, self.same_order, self.same_test_uv
)
}
}
fn ordered_loop_keys(fragment: &FaceFragmentRecord) -> Vec<Vec<String>> {
fragment.loops.iter().map(|l| l.coedges.iter().map(coedge_key).collect()).collect()
}
pub(super) fn fragments_agree(split: &[FaceFragmentRecord], arranged: &[FaceFragmentRecord]) -> Result<Agreement, String> {
if split.len() != arranged.len() {
return Err(format!("fragment count {} vs arrangement {}", split.len(), arranged.len()));
}
let mut agreement = Agreement { fragments: split.len(), same_order: 0, same_test_uv: 0 };
let mut unmatched: Vec<(usize, String, &FaceFragmentRecord)> =
arranged.iter().enumerate().map(|(at, fragment)| (at, fragment_key(fragment), fragment)).collect();
for (position, fragment) in split.iter().enumerate() {
let key = fragment_key(fragment);
let Some(found) = unmatched.iter().position(|(_, other, _)| *other == key) else {
let loops: Vec<String> = fragment
.loops
.iter()
.map(|l| {
l.coedges
.iter()
.map(|c| match &c.source {
FragmentEdgeSource::Boundary { edge_id, .. } => format!("e{edge_id}{}", if c.forward { "+" } else { "-" }),
FragmentEdgeSource::SharedBoundary { edge_id, .. } => format!("s{edge_id}{}", if c.forward { "+" } else { "-" }),
FragmentEdgeSource::Imprint { piece_id } => format!("p{piece_id}{}", if c.forward { "+" } else { "-" }),
FragmentEdgeSource::Derived { .. } => "derived".to_string(),
})
.collect::<Vec<_>>()
.join(",")
})
.collect();
let arranged_loops: Vec<String> = arranged
.iter()
.map(|f| {
f.loops
.iter()
.map(|l| {
l.coedges
.iter()
.map(|c| match &c.source {
FragmentEdgeSource::Boundary { edge_id, .. } => format!("e{edge_id}{}", if c.forward { "+" } else { "-" }),
FragmentEdgeSource::SharedBoundary { edge_id, .. } => format!("s{edge_id}{}", if c.forward { "+" } else { "-" }),
FragmentEdgeSource::Imprint { piece_id } => format!("p{piece_id}{}", if c.forward { "+" } else { "-" }),
FragmentEdgeSource::Derived { .. } => "derived".to_string(),
})
.collect::<Vec<_>>()
.join(",")
})
.collect::<Vec<_>>()
.join(" ; ")
})
.collect();
return Err(format!(
"split fragment {position} [{}] has no arrangement twin among [{}]",
loops.join(" ; "),
arranged_loops.join(" // ")
));
};
let (twin_position, _, twin) = unmatched.swap_remove(found);
let split_in_twin = record_contains(twin, fragment.test_uv).map_err(|e| e.to_string())?;
let twin_in_split = record_contains(fragment, twin.test_uv).map_err(|e| e.to_string())?;
if !split_in_twin || !twin_in_split {
return Err(format!(
"fragment {position}: test points disagree (split ({:.6},{:.6}) in arrangement region: {split_in_twin}; arrangement ({:.6},{:.6}) in split region: {twin_in_split})",
fragment.test_uv.x, fragment.test_uv.y, twin.test_uv.x, twin.test_uv.y
));
}
if twin_position == position && ordered_loop_keys(fragment) == ordered_loop_keys(twin) {
agreement.same_order += 1;
}
if fragment.test_uv.x == twin.test_uv.x && fragment.test_uv.y == twin.test_uv.y {
agreement.same_test_uv += 1;
}
}
Ok(agreement)
}
pub(super) fn loop_split_census(line: impl FnOnce() -> String) {
if let Ok(path) = std::env::var("BREP_LOOP_SPLIT_CENSUS") {
use std::io::Write;
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
let label = std::env::var("BREP_CENSUS_LABEL").unwrap_or_default();
let _ = writeln!(file, "loop-split census: label {label} {}", line());
}
}
}