use super::*;
pub(super) fn seam_meridian_samples(
face: &FaceRecord,
p_seam: f64,
p_is_u: bool,
q_lo: f64,
q_hi: f64,
chord_tolerance: f64,
wrap_q: bool,
) -> Result<Vec<(f64, Vec3)>, String> {
let eval = |q: f64| -> Result<Vec3, String> {
match (p_is_u, wrap_q) {
(true, false) => face.surface.evaluate(p_seam, q),
(true, true) => face.surface.evaluate_extended(p_seam, q),
(false, false) => face.surface.evaluate(q, p_seam),
(false, true) => face.surface.evaluate_extended(q, p_seam),
}
};
let mut qs = vec![q_lo, q_hi];
loop {
let mut refined = Vec::with_capacity(qs.len() * 2);
let mut split = false;
for pair in qs.windows(2) {
refined.push(pair[0]);
let mid = (pair[0] + pair[1]) * 0.5;
if deflection_to_chord(eval(mid)?, eval(pair[0])?, eval(pair[1])?) > chord_tolerance {
refined.push(mid);
split = true;
}
}
refined.push(*qs.last().unwrap());
qs = refined;
if !split || qs.len() > MAX_EDGE_SPANS {
break;
}
}
qs.into_iter().map(|q| Ok((q, eval(q)?))).collect()
}
pub(in crate::watertight_tessellation) fn tessellate_full_biperiodic_face(
face: &FaceRecord,
domain: [f64; 4],
chord_tolerance: f64,
face_id: u32,
mesh: &mut Mesh,
) -> Result<(), String> {
let [u0, u1, v0, v1] = domain;
let mut u_stations = Vec::new();
let mut v_stations = Vec::new();
for index in 0..=4 {
let fraction = index as f64 / 4.0;
let v = v0 + (v1 - v0) * fraction;
u_stations.extend(
seam_meridian_samples(face, v, false, u0, u1, chord_tolerance, false)?
.into_iter()
.map(|(u, _)| u),
);
let u = u0 + (u1 - u0) * fraction;
v_stations.extend(
seam_meridian_samples(face, u, true, v0, v1, chord_tolerance, false)?
.into_iter()
.map(|(v, _)| v),
);
}
let normalize = |stations: &mut Vec<f64>, span: f64| {
stations.sort_by(|a, b| a.total_cmp(b));
stations.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * span.abs().max(1.0));
stations.pop(); };
normalize(&mut u_stations, u1 - u0);
normalize(&mut v_stations, v1 - v0);
if u_stations.len() < 3 || v_stations.len() < 3 {
return Err("watertight tessellation: under-sampled full periodic face".into());
}
let (nu, nv) = (u_stations.len(), v_stations.len());
let base = (mesh.positions.len() / 3) as u32;
for &u in &u_stations {
for &v in &v_stations {
let point = face.surface.evaluate(u, v)?;
let normal = face_normal_at(face, [u, v], domain)?;
mesh.positions.extend([point.x, point.y, point.z]);
mesh.normals.extend([normal.x, normal.y, normal.z]);
}
}
let index = |u: usize, v: usize| base + (u * nv + v) as u32;
for u in 0..nu {
for v in 0..nv {
let a = index(u, v);
let b = index((u + 1) % nu, v);
let c = index((u + 1) % nu, (v + 1) % nv);
let d = index(u, (v + 1) % nv);
if face.same_sense {
mesh.indices.extend([a, b, c, a, c, d]);
} else {
mesh.indices.extend([a, c, b, a, d, c]);
}
mesh.face_ids.extend([face_id, face_id]);
}
}
Ok(())
}
#[derive(Clone)]
struct PeriodicRing {
level: f64,
points: Vec<FaceVertex>,
pole: bool,
direction: i32,
}
pub(in crate::watertight_tessellation) fn close_periodic_trim(
face: &FaceRecord,
chains: &[Vec<FaceVertex>],
domains: [f64; 4],
closed_u: bool,
closed_v: bool,
chord_tolerance: f64,
) -> Result<Option<(Vec<FaceVertex>, bool)>, String> {
if !closed_u && !closed_v {
return Ok(None);
}
for p_is_u in [true, false] {
if p_is_u && !closed_u {
continue;
}
if !p_is_u && !closed_v {
continue;
}
let q_closed = if p_is_u { closed_v } else { closed_u };
if let Some(result) =
close_periodic_trim_dir(face, chains, domains, p_is_u, q_closed, chord_tolerance)?
{
return Ok(Some(result));
}
}
Ok(None)
}
fn close_periodic_trim_dir(
face: &FaceRecord,
chains: &[Vec<FaceVertex>],
domains: [f64; 4],
p_is_u: bool,
q_closed: bool,
chord_tolerance: f64,
) -> Result<Option<(Vec<FaceVertex>, bool)>, String> {
let [u0, u1, v0, v1] = domains;
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 coord = |vertex: &FaceVertex| -> (f64, f64) {
if p_is_u {
(vertex.uv[0], vertex.uv[1])
} else {
(vertex.uv[1], vertex.uv[0])
}
};
let mut rings: Vec<PeriodicRing> = Vec::with_capacity(chains.len());
let mut holes: Vec<Vec<FaceVertex>> = Vec::new();
for chain in chains {
if chain.is_empty() {
return Ok(None);
}
let (mut pmin, mut pmax) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut qmin, mut qmax) = (f64::INFINITY, f64::NEG_INFINITY);
for vertex in chain {
let (p, q) = coord(vertex);
pmin = pmin.min(p);
pmax = pmax.max(p);
qmin = qmin.min(q);
qmax = qmax.max(q);
}
let mut net = 0.0;
for pair in chain.windows(2) {
let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
if delta > 0.5 * period {
delta -= period;
} else if delta < -0.5 * period {
delta += period;
}
net += delta;
}
let direction = if net > 0.25 * period {
1
} else if net < -0.25 * period {
-1
} else {
0
};
let coincident = chain
.iter()
.all(|vertex| vertex.position.sub(chain[0].position).length() <= 1e-9);
if coincident && (pmax - pmin) < 1e-3 * period && (qmax - qmin) < 1e-3 * q_extent {
rings.push(PeriodicRing {
level: coord(&chain[0]).1,
points: vec![chain[0]],
pole: true,
direction,
});
} else if (pmax - pmin) >= 0.6 * period && (qmax - qmin) <= 0.05 * q_extent {
let mut points = chain.clone();
points.sort_by(|a, b| coord(a).0.total_cmp(&coord(b).0));
let level = (qmin + qmax) * 0.5;
let epsilon = 1e-6 * period;
let uv_at = |p: f64| if p_is_u { [p, level] } else { [level, p] };
if coord(&points[0]).0 > p0 + epsilon {
let seam_position = points[points.len() - 1].position;
points.insert(
0,
FaceVertex {
uv: uv_at(p0),
position: seam_position,
},
);
}
if coord(&points[points.len() - 1]).0 < p1 - epsilon {
let seam_position = points[0].position;
points.push(FaceVertex {
uv: uv_at(p1),
position: seam_position,
});
}
rings.push(PeriodicRing {
level,
points,
pole: false,
direction,
});
} else {
if pmin <= p0 + 1e-6 * period || pmax >= p1 - 1e-6 * period {
return Ok(None);
}
holes.push(chain.clone());
}
}
if rings.len() != 2 || rings.iter().all(|ring| ring.pole) {
return Ok(None);
}
rings.sort_by(|a, b| a.level.total_cmp(&b.level));
let (lower_direction, upper_direction) = (rings[0].direction, rings[1].direction);
let inconclusive = lower_direction == 0
|| upper_direction == 0
|| lower_direction == upper_direction
|| rings.iter().any(|ring| ring.pole);
let between_rims_is_ccw_uv = if p_is_u {
lower_direction > 0
} else {
lower_direction < 0
};
let wraps_cross_seam = q_closed && !inconclusive && between_rims_is_ccw_uv != face.same_sense;
if std::env::var("BREP_DEBUG_TESS_BAND").is_ok() {
eprintln!(
"face {}: periodic rims p_is_u={p_is_u} q_closed={q_closed} \
senses=({lower_direction:+},{upper_direction:+}) same_sense={} levels {:.5}/{:.5} -> {}",
face.id,
face.same_sense,
rings[0].level,
rings[1].level,
if wraps_cross_seam { "CROSS-SEAM band" } else { "between-rims band" },
);
}
let q_period = q1 - q0;
let lift_ring = |ring: &PeriodicRing| -> PeriodicRing {
let mut lifted = ring.clone();
lifted.level += q_period;
for vertex in &mut lifted.points {
if p_is_u {
vertex.uv[1] += q_period;
} else {
vertex.uv[0] += q_period;
}
}
lifted
};
let rings: [PeriodicRing; 2] = if wraps_cross_seam {
[rings[1].clone(), lift_ring(&rings[0])]
} else {
[rings[0].clone(), rings[1].clone()]
};
let (q_lo, q_hi) = (rings[0].level, rings[1].level);
let mk_uv = |p: f64, q: f64| if p_is_u { [p, q] } else { [q, p] };
let seam = seam_meridian_samples(
face,
p1,
p_is_u,
q_lo,
q_hi,
chord_tolerance,
wraps_cross_seam,
)?;
let boundary_at = |ring: &PeriodicRing| -> Vec<FaceVertex> {
if ring.pole {
let position = ring.points[0].position;
vec![
FaceVertex {
uv: mk_uv(p0, ring.level),
position,
},
FaceVertex {
uv: mk_uv(p1, ring.level),
position,
},
]
} else {
ring.points.clone()
}
};
let mut polygon: Vec<FaceVertex> = Vec::new();
polygon.extend(boundary_at(&rings[0]));
if seam.len() > 2 {
for &(q, position) in &seam[1..seam.len() - 1] {
polygon.push(FaceVertex {
uv: mk_uv(p1, q),
position,
});
}
}
let mut top = boundary_at(&rings[1]);
top.reverse();
polygon.extend(top);
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 signed_area(&polygon) < 0.0 {
polygon.reverse();
}
if !holes.is_empty() {
let holes: Vec<Vec<FaceVertex>> = holes
.into_iter()
.map(|mut hole| {
if signed_area(&hole) > 0.0 {
hole.reverse();
}
hole
})
.collect();
polygon = bridge_holes(polygon, holes);
}
Ok(Some((polygon, wraps_cross_seam)))
}