use super::*;
struct BandBoundary {
points: Vec<FaceVertex>,
seam_q: f64,
qmin: f64,
qmax: f64,
}
fn classify_band_boundary(
chain: &[FaceVertex],
p_is_u: bool,
p0: f64,
p1: f64,
) -> Option<BandBoundary> {
let period = p1 - p0;
if !(period > 0.0) || chain.len() < 2 {
return None;
}
let coord = |vertex: &FaceVertex| -> (f64, f64) {
if p_is_u {
(vertex.uv[0], vertex.uv[1])
} else {
(vertex.uv[1], vertex.uv[0])
}
};
let mut offset = 0.0f64;
let mut previous = coord(&chain[0]).0;
let mut unwrapped: Vec<f64> = Vec::with_capacity(chain.len());
for (index, vertex) in chain.iter().enumerate() {
let raw = coord(vertex).0;
if index > 0 {
let delta = raw - previous;
if delta > 0.5 * period {
offset -= period;
} else if delta < -0.5 * period {
offset += period;
}
}
previous = raw;
unwrapped.push(raw + offset);
}
let closing = coord(&chain[0]).0 - previous;
let closing_offset = if closing > 0.5 * period {
offset - period
} else if closing < -0.5 * period {
offset + period
} else {
offset
};
if (closing_offset.abs() - period).abs() > 0.25 * period {
return None;
}
let eps = 1e-6 * period;
let mut sign = 0i32;
for pair in unwrapped.windows(2) {
let delta = pair[1] - pair[0];
let this = if delta > eps {
1
} else if delta < -eps {
-1
} else {
0
};
if this != 0 {
if sign != 0 && this != sign {
return None;
}
sign = this;
}
}
if sign == 0 {
return None;
}
let mut points: Vec<FaceVertex> = chain.to_vec();
points.sort_by(|a, b| coord(a).0.total_cmp(&coord(b).0));
let (mut qmin, mut qmax) = (f64::INFINITY, f64::NEG_INFINITY);
for vertex in &points {
let q = coord(vertex).1;
qmin = qmin.min(q);
qmax = qmax.max(q);
}
let front_gap = coord(&points[0]).0 - p0;
let back_gap = p1 - coord(points.last().unwrap()).0;
let seam_vertex = if front_gap <= back_gap {
points[0]
} else {
*points.last().unwrap()
};
let seam_q = coord(&seam_vertex).1;
let seam_uv = |p: f64| -> [f64; 2] {
if p_is_u {
[p, seam_q]
} else {
[seam_q, p]
}
};
let corner_eps = 1e-6 * period;
let front_position = points[0].position;
let back_position = points.last().unwrap().position;
if back_gap > corner_eps {
points.push(FaceVertex {
uv: seam_uv(p1),
position: front_position,
});
}
if front_gap > corner_eps {
points.insert(
0,
FaceVertex {
uv: seam_uv(p0),
position: back_position,
},
);
}
Some(BandBoundary {
points,
seam_q,
qmin,
qmax,
})
}
pub(in crate::watertight_tessellation) fn close_periodic_band(
face: &FaceRecord,
chains: &[Vec<FaceVertex>],
domains: [f64; 4],
closed_u: bool,
closed_v: bool,
chord_tolerance: f64,
) -> Result<Option<Vec<FaceVertex>>, String> {
if chains.len() != 2 || (!closed_u && !closed_v) {
return Ok(None);
}
let torus = closed_u && closed_v;
let [u0, u1, v0, v1] = domains;
let p_is_u = closed_u;
let (p0, p1) = if p_is_u { (u0, u1) } else { (v0, v1) };
let (q0, q1) = if p_is_u { (v0, v1) } else { (u0, u1) };
let period = p1 - p0;
let q_extent = (q1 - q0).abs().max(1e-30);
if !(period > 0.0) {
return Ok(None);
}
let mut boundaries: Vec<BandBoundary> = Vec::with_capacity(2);
for chain in chains {
match classify_band_boundary(chain, p_is_u, p0, p1) {
Some(boundary) => boundaries.push(boundary),
None => return Ok(None),
}
}
let band_qlo = boundaries.iter().map(|b| b.qmin).fold(f64::INFINITY, f64::min);
let band_qhi = boundaries.iter().map(|b| b.qmax).fold(f64::NEG_INFINITY, f64::max);
if torus {
let margin = 0.02 * q_extent;
if band_qlo <= q0 + margin || band_qhi >= q1 - margin {
return Ok(None);
}
}
if !torus {
let cross_extent = (q1 - q0).abs();
let mut cross_second: f64 = 0.0;
for i in 0..=4 {
for j in 0..=4 {
let u = u0 + (u1 - u0) * i as f64 / 4.0;
let v = v0 + (v1 - v0) * j as f64 / 4.0;
let derivatives = face.surface.derivatives(u, v, 2)?;
let second = if p_is_u {
derivatives[0][2]
} else {
derivatives[2][0]
};
cross_second = cross_second.max(second.length());
}
}
if cross_second * cross_extent * cross_extent > 8.0 * chord_tolerance {
return Ok(None);
}
}
let is_flat = |b: &BandBoundary| (b.qmax - b.qmin) <= 0.05 * q_extent;
let flat_count = boundaries.iter().filter(|b| is_flat(b)).count();
if flat_count != 1 {
return Ok(None);
}
boundaries.sort_by(|a, b| {
let ka = 0.5 * (a.qmin + a.qmax);
let kb = 0.5 * (b.qmin + b.qmax);
ka.total_cmp(&kb)
});
let margin = 1e-6 * q_extent;
if boundaries[0].qmax > boundaries[1].qmin + margin {
return Ok(None);
}
let mk_uv = |p: f64, q: f64| if p_is_u { [p, q] } else { [q, p] };
let (lo_seam_q, hi_seam_q) = (boundaries[0].seam_q, boundaries[1].seam_q);
let seam = seam_meridian_samples(
face,
p1,
p_is_u,
lo_seam_q,
hi_seam_q,
chord_tolerance,
false,
)?;
let mut polygon: Vec<FaceVertex> = Vec::new();
polygon.extend(boundaries[0].points.iter().copied());
if seam.len() > 2 {
for &(q, position) in &seam[1..seam.len() - 1] {
polygon.push(FaceVertex {
uv: mk_uv(p1, q),
position,
});
}
}
for vertex in boundaries[1].points.iter().rev() {
polygon.push(*vertex);
}
if seam.len() > 2 {
for &(q, position) in seam[1..seam.len() - 1].iter().rev() {
polygon.push(FaceVertex {
uv: mk_uv(p0, q),
position,
});
}
}
if polygon.len() < 3 {
return Ok(None);
}
if !polygon_is_simple(&polygon) {
return Ok(None);
}
if signed_area(&polygon) < 0.0 {
polygon.reverse();
}
Ok(Some(polygon))
}
fn lift_loop_cycle(
chain: &[FaceVertex],
start: usize,
p_is_u: bool,
period: f64,
p_start: f64,
) -> Vec<FaceVertex> {
let coord = |vertex: &FaceVertex| -> (f64, f64) {
if p_is_u {
(vertex.uv[0], vertex.uv[1])
} else {
(vertex.uv[1], vertex.uv[0])
}
};
let count = chain.len();
let mut lifted = Vec::with_capacity(count + 1);
let mut previous = coord(&chain[start]).0;
let mut current = p_start;
for step in 0..=count {
let vertex = &chain[(start + step) % count];
let (raw, q) = coord(vertex);
if step > 0 {
let mut delta = raw - previous;
if delta > 0.5 * period {
delta -= period;
} else if delta < -0.5 * period {
delta += period;
}
current += delta;
previous = raw;
}
let uv = if p_is_u { [current, q] } else { [q, current] };
lifted.push(FaceVertex {
uv,
position: vertex.position,
});
}
lifted
}
pub(in crate::watertight_tessellation) fn close_periodic_spiral_annulus(
face: &FaceRecord,
chains: &[Vec<FaceVertex>],
domains: [f64; 4],
closed_u: bool,
closed_v: bool,
) -> Result<Option<Vec<FaceVertex>>, String> {
let reject = |reason: &str| -> Result<Option<Vec<FaceVertex>>, String> {
if std::env::var("BREP_DEBUG_TESS_BAND").is_ok() {
eprintln!("face {}: not a spiral annulus ({reason})", face.id);
}
Ok(None)
};
if closed_u == closed_v || chains.len() != 2 {
return Ok(None);
}
let [u0, u1, v0, v1] = domains;
let p_is_u = closed_u;
let (p0, p1) = if p_is_u { (u0, u1) } else { (v0, v1) };
let (q0, q1) = if p_is_u { (v0, v1) } else { (u0, u1) };
let period = p1 - p0;
let q_extent = (q1 - q0).abs();
if !(period > 0.0) || !(q_extent > 0.0) {
return Ok(None);
}
let coord = |vertex: &FaceVertex| -> (f64, f64) {
if p_is_u {
(vertex.uv[0], vertex.uv[1])
} else {
(vertex.uv[1], vertex.uv[0])
}
};
struct LoopShape {
winding: i32,
qmin: f64,
qmax: f64,
}
let mut shapes = Vec::with_capacity(2);
for chain in chains {
if chain.len() < 2 {
return Ok(None);
}
let mut net = 0.0;
let (mut qmin, mut qmax) = (f64::INFINITY, f64::NEG_INFINITY);
for index in 0..chain.len() {
let (raw, q) = coord(&chain[index]);
qmin = qmin.min(q);
qmax = qmax.max(q);
let next = coord(&chain[(index + 1) % chain.len()]).0;
let mut delta = next - raw;
if delta > 0.5 * period {
delta -= period;
} else if delta < -0.5 * period {
delta += period;
}
net += delta;
}
let turns = net / period;
if (turns.abs() - 1.0).abs() > 0.25 {
return reject(&format!(
"loop winding {turns:.3} is not a single full wrap"
));
}
shapes.push(LoopShape {
winding: if turns > 0.0 { 1 } else { -1 },
qmin,
qmax,
});
}
if shapes[0].winding == shapes[1].winding {
return reject("both loops wind the same way (not an annulus)");
}
let flat = |shape: &LoopShape| (shape.qmax - shape.qmin) <= 0.05 * q_extent;
let rim_index = match (flat(&shapes[0]), flat(&shapes[1])) {
(true, false) => 0,
(false, true) => 1,
_ => return reject("need exactly one constant-level rim loop"),
};
let ribbon_index = 1 - rim_index;
let (rim, ribbon) = (&chains[rim_index], &chains[ribbon_index]);
let margin = 1e-9 * q_extent;
let rim_below = shapes[rim_index].qmax + margin < shapes[ribbon_index].qmin;
let rim_above = shapes[rim_index].qmin - margin > shapes[ribbon_index].qmax;
if !rim_below && !rim_above {
return reject(&format!(
"rim q [{:.5},{:.5}] overlaps ribbon q [{:.5},{:.5}]",
shapes[rim_index].qmin,
shapes[rim_index].qmax,
shapes[ribbon_index].qmin,
shapes[ribbon_index].qmax
));
}
let better = |a: f64, b: f64| if rim_below { a < b } else { a > b };
let mut cut_index = 0;
for index in 1..ribbon.len() {
if better(coord(&ribbon[index]).1, coord(&ribbon[cut_index]).1) {
cut_index = index;
}
}
let cut_level = coord(&ribbon[cut_index]).1;
let cut_p = coord(&ribbon[cut_index]).0;
let ribbon_lifted = lift_loop_cycle(ribbon, cut_index, p_is_u, period, cut_p);
let landing = coord(ribbon_lifted.last().unwrap()).0;
let mut rim_index_start = 0;
let mut best = f64::INFINITY;
for (index, vertex) in rim.iter().enumerate() {
let raw = coord(vertex).0;
let offset = ((landing - raw) / period).round() * period;
let distance = (raw + offset - landing).abs();
if distance < best {
best = distance;
rim_index_start = index;
}
}
let rim_raw = coord(&rim[rim_index_start]).0;
let rim_start = rim_raw + ((landing - rim_raw) / period).round() * period;
let rim_lifted = lift_loop_cycle(rim, rim_index_start, p_is_u, period, rim_start);
let mut polygon = ribbon_lifted;
polygon.extend(rim_lifted);
if polygon.len() < 3 {
return Ok(None);
}
if !polygon_is_simple(&polygon) {
return reject("lifted polygon self-intersects");
}
if std::env::var("BREP_DEBUG_TESS_BAND").is_ok() {
eprintln!(
"face {}: spiral annulus, ribbon {} coedge samples over {:.2} periods, rim {} samples \
{}, cut at ({:.4},{:.4})",
face.id,
ribbon.len(),
{
let ps: Vec<f64> = polygon.iter().map(|v| coord(v).0).collect();
(ps.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
- ps.iter().cloned().fold(f64::INFINITY, f64::min))
/ period
},
rim.len(),
if rim_below { "below" } else { "above" },
cut_p,
cut_level,
);
}
if signed_area(&polygon) < 0.0 {
polygon.reverse();
}
Ok(Some(polygon))
}