use super::*;
const MAX_GRID_LINES: usize = 512;
const MAX_GRID_PASSES: usize = 10;
const SAMPLE_AGREEMENT: f64 = 1e-7;
fn decline(face_id: u32, why: &str) -> Result<bool, String> {
if std::env::var("BREP_DEBUG_TESS_DISPATCH").is_ok() {
eprintln!("[pole-patch] face {face_id}: DECLINED — {why}");
}
Ok(false)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Side {
BottomPole,
SeamUp,
TopPole,
SeamDown,
}
fn pcurve_ends(coedge: &CoedgeRecord) -> Result<([f64; 2], [f64; 2]), String> {
let [t0, t1] = coedge.pcurve.domain()?;
let a = coedge.pcurve.evaluate(t0)?;
let b = coedge.pcurve.evaluate(t1)?;
Ok(([a.x, a.y], [b.x, b.y]))
}
fn classify(coedge: &CoedgeRecord, domains: [f64; 4], degenerate: bool) -> Result<Option<Side>, String> {
let [u0, u1, v0, v1] = domains;
let tol = 1e-9 * ((u1 - u0).abs() + (v1 - v0).abs());
let (a, b) = pcurve_ends(coedge)?;
let near = |x: f64, y: f64| (x - y).abs() <= tol;
let side = if degenerate {
if near(a[1], v0) && near(b[1], v0) {
Side::BottomPole
} else if near(a[1], v1) && near(b[1], v1) {
Side::TopPole
} else {
return Ok(None);
}
} else if near(a[0], u1) && near(b[0], u1) && near(a[1], v0) && near(b[1], v1) {
Side::SeamUp
} else if near(a[0], u0) && near(b[0], u0) && near(a[1], v1) && near(b[1], v0) {
Side::SeamDown
} else {
return Ok(None);
};
Ok(Some(side))
}
pub(super) fn tessellate_pole_patch_watertight(
face: &FaceRecord,
samples: &HashMap<u64, EdgeSamples>,
chord_tolerance: f64,
face_id: u32,
mesh: &mut Mesh,
) -> Result<bool, String> {
if std::env::var("BREP_NO_POLE_PATCH").is_ok() {
return Ok(false);
}
if face.surface.analytic().is_some() {
return Ok(false);
}
if face.loops.len() != 1 || face.loops[0].coedges.len() != 4 {
return Ok(false);
}
let (closed_u, closed_v) = face.surface.closed_directions()?;
if !closed_u || closed_v {
return Ok(false);
}
let ku = KnotVector::new(face.surface.knots_u.clone(), face.surface.degree_u)?;
let kv = KnotVector::new(face.surface.knots_v.clone(), face.surface.degree_v)?;
let [u0, u1] = ku.domain();
let [v0, v1] = kv.domain();
let domains = [u0, u1, v0, v1];
let coedges = &face.loops[0].coedges;
let mut sides = Vec::with_capacity(4);
for coedge in coedges {
let edge_samples = samples
.get(&coedge.edge_id)
.ok_or("pole patch: missing edge samples")?;
let degenerate = edge_samples
.positions
.windows(2)
.all(|pair| pair[0].sub(pair[1]).length() == 0.0);
match classify(coedge, domains, degenerate)? {
Some(side) => sides.push((side, coedge)),
None => return decline(face_id, "a coedge is not a pole or seam side"),
}
}
let find = |wanted: Side| sides.iter().find(|(side, _)| *side == wanted).map(|(_, c)| *c);
let (Some(bottom), Some(up), Some(top), Some(down)) =
(find(Side::BottomPole), find(Side::SeamUp), find(Side::TopPole), find(Side::SeamDown))
else {
return decline(face_id, "the loop does not carry each side once");
};
if up.edge_id != down.edge_id || up.forward == down.forward {
return decline(face_id, "the two seam coedges do not share one edge in opposite senses");
}
if bottom.edge_id == top.edge_id {
return decline(face_id, "both poles name the same edge");
}
let seam = &samples[&up.edge_id];
let mut extent = 0.0f64;
for row in &face.surface.control_points {
for cp in row {
let w = if cp.w != 0.0 { cp.w } else { 1.0 };
extent = extent.max((cp.x / w).abs()).max((cp.y / w).abs()).max((cp.z / w).abs());
}
}
let agreement = SAMPLE_AGREEMENT * extent.max(1e-9);
let seam_v: Vec<f64> = if up.forward {
seam.fractions.iter().map(|f| v0 + f * (v1 - v0)).collect()
} else {
seam.fractions.iter().rev().map(|f| v1 - f * (v1 - v0)).collect()
};
let seam_positions: Vec<Vec3> = if up.forward {
seam.positions.clone()
} else {
seam.positions.iter().rev().copied().collect()
};
if seam_v.len() < 2 || seam_v.len() != seam_positions.len() {
return decline(face_id, "the seam carries no samples");
}
for (v, position) in seam_v.iter().zip(&seam_positions) {
if face.surface.evaluate(u0, *v)?.sub(*position).length() > agreement {
return decline(face_id, "a seam sample is off the surface at u0");
}
}
let pole_bottom = samples[&bottom.edge_id].positions[0];
let pole_top = samples[&top.edge_id].positions[0];
if face.surface.evaluate(u0, v0)?.sub(pole_bottom).length() > agreement
|| face.surface.evaluate(u0, v1)?.sub(pole_top).length() > agreement
{
return decline(face_id, "a pole vertex is off the surface");
}
let mut cols: Vec<f64> = vec![u0];
for knot in &face.surface.knots_u {
if *knot > u0 && *knot < u1 && cols.last().is_some_and(|last| knot - last > 1e-12 * (u1 - u0)) {
cols.push(*knot);
}
}
cols.push(u1);
let mut rows: Vec<f64> = seam_v.clone();
rows.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * (v1 - v0));
for _pass in 0..MAX_GRID_PASSES {
let mut split_cols = vec![false; cols.len() - 1];
let mut split_rows = vec![false; rows.len() - 1];
let mut corners: Vec<Vec<Vec3>> = Vec::with_capacity(cols.len());
for u in &cols {
let mut column = Vec::with_capacity(rows.len());
for v in &rows {
column.push(face.surface.evaluate(*u, *v)?);
}
corners.push(column);
}
for ci in 0..cols.len() - 1 {
let um = 0.5 * (cols[ci] + cols[ci + 1]);
for ri in 0..rows.len() - 1 {
let vm = 0.5 * (rows[ri] + rows[ri + 1]);
let a = corners[ci][ri];
let b = corners[ci + 1][ri];
let c = corners[ci + 1][ri + 1];
let d = corners[ci][ri + 1];
if !split_cols[ci] {
let (row_v, pa, pb) = if b.sub(a).length() >= c.sub(d).length() {
(rows[ri], a, b)
} else {
(rows[ri + 1], d, c)
};
let mid = face.surface.evaluate(um, row_v)?;
if deflection_to_chord(mid, pa, pb) > chord_tolerance {
split_cols[ci] = true;
}
}
if !split_rows[ri] {
let (col_u, pa, pd) = if d.sub(a).length() >= c.sub(b).length() {
(cols[ci], a, d)
} else {
(cols[ci + 1], b, c)
};
let mid = face.surface.evaluate(col_u, vm)?;
if deflection_to_chord(mid, pa, pd) > chord_tolerance {
split_rows[ri] = true;
}
}
if !split_cols[ci] || !split_rows[ri] {
let centre = face.surface.evaluate(um, vm)?;
let sag = deflection_to_chord(centre, a, c).min(deflection_to_chord(centre, b, d));
if sag > chord_tolerance {
if cols[ci + 1] - cols[ci] >= rows[ri + 1] - rows[ri] {
split_cols[ci] = true;
} else {
split_rows[ri] = true;
}
}
}
}
}
let any_col = split_cols.iter().any(|s| *s);
let any_row = split_rows.iter().any(|s| *s);
if !any_col && !any_row {
break;
}
if any_col && cols.len() < MAX_GRID_LINES {
let mut refined = Vec::with_capacity(cols.len() * 2);
for (ci, u) in cols.iter().enumerate() {
refined.push(*u);
if ci + 1 < cols.len() && split_cols[ci] {
refined.push(0.5 * (cols[ci] + cols[ci + 1]));
}
}
cols = refined;
}
if any_row && rows.len() < MAX_GRID_LINES {
let mut refined = Vec::with_capacity(rows.len() * 2);
for (ri, v) in rows.iter().enumerate() {
refined.push(*v);
if ri + 1 < rows.len() && split_rows[ri] {
refined.push(0.5 * (rows[ri] + rows[ri + 1]));
}
}
rows = refined;
}
if cols.len() >= MAX_GRID_LINES && rows.len() >= MAX_GRID_LINES {
break;
}
}
let nc = cols.len() - 1; let nr = rows.len();
let base = (mesh.positions.len() / 3) as u32;
let mut push = |position: Vec3, normal: Vec3| -> u32 {
mesh.positions.extend([position.x, position.y, position.z]);
mesh.normals.extend([normal.x, normal.y, normal.z]);
(mesh.positions.len() / 3 - 1) as u32
};
let pole_normal = |v: f64| -> Result<Vec3, String> {
let inset = if v == v0 { v0 + 1e-4 * (v1 - v0) } else { v1 - 1e-4 * (v1 - v0) };
let mut sum = Vec3::default();
for u in &cols[..nc] {
sum = sum.add(face_normal_at(face, [*u, inset], domains)?);
}
match sum.normalized() {
Ok(n) => Ok(n),
Err(_) => face_normal_at(face, [u0, inset], domains),
}
};
let bottom_index = push(pole_bottom, pole_normal(v0)?);
let top_index = push(pole_top, pole_normal(v1)?);
let mut interior: Vec<Vec<u32>> = Vec::with_capacity(nr.saturating_sub(2));
for ri in 1..nr.saturating_sub(1) {
let mut row = Vec::with_capacity(nc);
for (ci, u) in cols.iter().take(nc).enumerate() {
let position = if ci == 0 {
seam_v
.iter()
.position(|sv| (*sv - rows[ri]).abs() <= 1e-12 * (v1 - v0))
.map(|i| seam_positions[i])
.unwrap_or(face.surface.evaluate(*u, rows[ri])?)
} else {
face.surface.evaluate(*u, rows[ri])?
};
let normal = face_normal_at(face, [*u, rows[ri]], domains)?;
row.push(push(position, normal));
}
interior.push(row);
}
let vertex = |ri: usize, ci: usize| -> u32 {
if ri == 0 {
bottom_index
} else if ri == nr - 1 {
top_index
} else {
interior[ri - 1][ci % nc]
}
};
let position_of = |index: u32| -> Vec3 {
let i = (index - base) as usize * 3;
Vec3::new(mesh.positions[i], mesh.positions[i + 1], mesh.positions[i + 2])
};
let mut triangles: Vec<[u32; 3]> = Vec::with_capacity(nc * nr * 2);
for ri in 0..nr - 1 {
for ci in 0..nc {
let a = vertex(ri, ci);
let b = vertex(ri, ci + 1);
let c = vertex(ri + 1, ci + 1);
let d = vertex(ri + 1, ci);
triangles.push([a, b, c]);
triangles.push([a, c, d]);
}
}
for [a, b, c] in triangles {
if a == b || b == c || c == a {
continue;
}
let (pa, pb, pc) = (position_of(a), position_of(b), position_of(c));
if pa.sub(pb).length() == 0.0 || pb.sub(pc).length() == 0.0 || pc.sub(pa).length() == 0.0 {
continue;
}
if face.same_sense {
mesh.indices.extend([a, b, c]);
} else {
mesh.indices.extend([a, c, b]);
}
mesh.face_ids.push(face_id);
}
if std::env::var("BREP_DEBUG_TESS_DISPATCH").is_ok() {
eprintln!(
"[pole-patch] face {face_id}: {} columns × {} rows, {} triangles",
nc,
nr,
mesh.face_ids.iter().filter(|id| **id == face_id).count()
);
}
Ok(true)
}