use brepkit_math::det_hash::{DetHashMap, DetHashSet};
use brepkit_math::vec::{Point3, Vec3};
use brepkit_topology::Topology;
use brepkit_topology::edge::EdgeCurve;
use brepkit_topology::face::{FaceId, FaceSurface};
use std::f64::consts::TAU;
use super::edge_sampling::{sample_edge, segments_for_chord_deviation_a};
use super::{MERGE_GRID, TriangleMesh, point_merge_key};
type ProjectFn = Box<dyn Fn(Point3) -> (f64, f64)>;
type EvalFn = Box<dyn Fn(f64, f64) -> Point3>;
type NormalFn = Box<dyn Fn(f64, f64) -> Vec3>;
pub(super) fn tessellate_band_face_local(
topo: &Topology,
face_data: &brepkit_topology::face::Face,
deflection: f64,
angular_tol: f64,
) -> Result<Option<super::TriangleMeshUV>, crate::OperationsError> {
if !face_data.inner_wires().is_empty() {
return Ok(None);
}
let (project, surf_normal): (ProjectFn, NormalFn) = match face_data.surface() {
FaceSurface::Cylinder(c) => {
let (c1, c2) = (c.clone(), c.clone());
(
Box::new(move |p| c1.project_point(p)),
Box::new(move |u, v| c2.normal(u, v)),
)
}
FaceSurface::Cone(c) => {
let (c1, c2) = (c.clone(), c.clone());
(
Box::new(move |p| c1.project_point(p)),
Box::new(move |u, v| c2.normal(u, v)),
)
}
_ => return Ok(None),
};
let wire = topo.wire(face_data.outer_wire())?;
let mut curved: Vec<(
brepkit_topology::edge::EdgeId,
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
)> = Vec::new();
let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
for oe in wire.edges() {
let e = topo.edge(oe.edge())?;
match e.curve() {
EdgeCurve::NurbsCurve(_) if e.start() == e.end() => return Ok(None),
EdgeCurve::Circle(_) | EdgeCurve::NurbsCurve(_) => {
if seen.insert(oe.edge().index()) {
curved.push((oe.edge(), e.start(), e.end()));
}
}
EdgeCurve::Line => {}
EdgeCurve::Ellipse(_) => return Ok(None),
}
}
let mut by_vertex: std::collections::HashMap<brepkit_topology::vertex::VertexId, Vec<usize>> =
std::collections::HashMap::new();
for (j, &(_, sv, ev)) in curved.iter().enumerate() {
by_vertex.entry(sv).or_default().push(j);
by_vertex.entry(ev).or_default().push(j);
}
let mut used = vec![false; curved.len()];
let mut cycles: Vec<Vec<usize>> = Vec::new();
for start in 0..curved.len() {
if used[start] {
continue;
}
let (_, origin, mut at) = curved[start];
used[start] = true;
let mut cycle = vec![start];
let mut closed = curved[start].1 == curved[start].2 || at == origin;
while !closed {
let Some(&next) = by_vertex
.get(&at)
.and_then(|c| c.iter().find(|&&j| !used[j]))
else {
break;
};
used[next] = true;
at = if curved[next].1 == at {
curved[next].2
} else {
curved[next].1
};
cycle.push(next);
closed = at == origin;
}
if !closed {
return Ok(None);
}
cycles.push(cycle);
}
if cycles.len() != 2 {
return Ok(None);
}
let wrap_pi = |d: f64| -> f64 { (d + TAU / 2.0).rem_euclid(TAU) - TAU / 2.0 };
for cycle in &cycles {
let mut winding = 0.0_f64;
let mut whole_turn = false;
let mut at: Option<brepkit_topology::vertex::VertexId> = None;
for &ci in cycle {
let (_, sv, ev) = curved[ci];
if sv == ev {
whole_turn = true;
continue;
}
let (from, to) = match at {
None => (sv, ev),
Some(v) if v == sv => (sv, ev),
Some(_) => (ev, sv),
};
let (u0, _) = project(topo.vertex(from)?.point());
let (u1, _) = project(topo.vertex(to)?.point());
winding += wrap_pi(u1 - u0);
at = Some(to);
}
if !whole_turn && (winding.abs() - TAU).abs() > 1e-6 {
return Ok(None);
}
}
let mut rims: Vec<Vec<Point3>> = Vec::with_capacity(2);
for cycle in &cycles {
let mut pts: Vec<Point3> = Vec::new();
let mut keys: std::collections::HashSet<(i64, i64, i64)> = std::collections::HashSet::new();
for &ci in cycle {
let edge = topo.edge(curved[ci].0)?;
for p in sample_edge(topo, edge, deflection, angular_tol, false)? {
let k = point_merge_key(p, MERGE_GRID);
if keys.insert(k) {
pts.push(p);
}
}
}
if pts.len() < 3 {
return Ok(None);
}
pts.sort_by(|a, b| {
project(*a)
.0
.partial_cmp(&project(*b).0)
.unwrap_or(std::cmp::Ordering::Equal)
});
rims.push(pts);
}
let n = rims[0].len();
let m = rims[1].len();
let mut positions: Vec<Point3> = Vec::with_capacity(n + m);
positions.extend_from_slice(&rims[0]);
positions.extend_from_slice(&rims[1]);
let mut normals: Vec<Vec3> = Vec::with_capacity(n + m);
let mut uvs: Vec<[f64; 2]> = Vec::with_capacity(n + m);
for p in &positions {
let (u, v) = project(*p);
normals.push(surf_normal(u, v));
uvs.push([u, v]);
}
let ang = |i: usize| -> f64 { uvs[i][0] };
let base = ang(0);
let start1 = (0..m)
.min_by(|&a, &b| {
let ka = (ang(n + a) - base).rem_euclid(TAU);
let kb = (ang(n + b) - base).rem_euclid(TAU);
ka.partial_cmp(&kb).unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(0);
let ring0: Vec<usize> = (0..n).collect();
let mut ring1: Vec<usize> = (n..n + m).collect();
ring1.rotate_left(start1);
let unwrap = |a: f64| (a - base).rem_euclid(TAU);
let a0: Vec<f64> = ring0.iter().map(|&i| unwrap(ang(i))).collect();
let a1: Vec<f64> = ring1.iter().map(|&i| unwrap(ang(i))).collect();
let mut indices: Vec<u32> = Vec::with_capacity((n + m) * 3);
let mut emit = |a: usize, b: usize, c: usize| {
let (pa, pb, pc) = (positions[a], positions[b], positions[c]);
let geo = (pb - pa).cross(pc - pa);
if geo.length() < 1e-20 {
return;
}
let (u, v) = project(pa);
let outward = surf_normal(u, v);
#[allow(clippy::cast_possible_truncation)]
let mut tri = [a as u32, b as u32, c as u32];
if geo.dot(outward) < 0.0 {
tri.swap(1, 2);
}
indices.extend_from_slice(&tri);
};
let (mut i, mut j) = (0usize, 0usize);
let (mut done0, mut done1) = (0usize, 0usize);
while done0 < n || done1 < m {
let next0 = if done0 >= n {
f64::INFINITY
} else if i + 1 < n {
a0[i + 1]
} else {
a0[0] + TAU
};
let next1 = if done1 >= m {
f64::INFINITY
} else if j + 1 < m {
a1[j + 1]
} else {
a1[0] + TAU
};
if next0 <= next1 {
let ni = (i + 1) % n;
emit(ring0[i], ring1[j], ring0[ni]);
i = ni;
done0 += 1;
} else {
let nj = (j + 1) % m;
emit(ring0[i], ring1[j], ring1[nj]);
j = nj;
done1 += 1;
}
}
Ok(Some(super::TriangleMeshUV {
mesh: TriangleMesh {
positions,
normals,
indices,
},
uvs,
}))
}
pub(super) fn tessellate_revolution_band_shared(
topo: &Topology,
face_data: &brepkit_topology::face::Face,
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &mut TriangleMesh,
) -> Result<bool, crate::OperationsError> {
if !face_data.inner_wires().is_empty() {
return Ok(false);
}
let (project, surf_normal): (ProjectFn, NormalFn) = match face_data.surface() {
FaceSurface::Cylinder(c) => {
let (c1, c2) = (c.clone(), c.clone());
(
Box::new(move |p| c1.project_point(p)),
Box::new(move |u, v| c2.normal(u, v)),
)
}
FaceSurface::Cone(c) => {
let (c1, c2) = (c.clone(), c.clone());
(
Box::new(move |p| c1.project_point(p)),
Box::new(move |u, v| c2.normal(u, v)),
)
}
_ => return Ok(false),
};
let wire = topo.wire(face_data.outer_wire())?;
let mut curved: Vec<(
usize,
brepkit_topology::vertex::VertexId,
brepkit_topology::vertex::VertexId,
)> = Vec::new();
let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
for oe in wire.edges() {
let e = topo.edge(oe.edge())?;
match e.curve() {
EdgeCurve::NurbsCurve(_) if e.start() == e.end() => return Ok(false),
EdgeCurve::Circle(_) | EdgeCurve::NurbsCurve(_) => {
if seen.insert(oe.edge().index()) {
curved.push((oe.edge().index(), e.start(), e.end()));
}
}
EdgeCurve::Line => {}
EdgeCurve::Ellipse(_) => return Ok(false),
}
}
let mut by_vertex: std::collections::HashMap<brepkit_topology::vertex::VertexId, Vec<usize>> =
std::collections::HashMap::new();
for (j, &(_, sv, ev)) in curved.iter().enumerate() {
by_vertex.entry(sv).or_default().push(j);
by_vertex.entry(ev).or_default().push(j);
}
let mut used = vec![false; curved.len()];
let mut cycles: Vec<Vec<usize>> = Vec::new();
for start in 0..curved.len() {
if used[start] {
continue;
}
let (_, origin, mut at) = curved[start];
used[start] = true;
let mut cycle = vec![start];
let mut closed = curved[start].1 == curved[start].2 || at == origin;
while !closed {
let Some(&next) = by_vertex
.get(&at)
.and_then(|c| c.iter().find(|&&j| !used[j]))
else {
break;
};
used[next] = true;
at = if curved[next].1 == at {
curved[next].2
} else {
curved[next].1
};
cycle.push(next);
closed = at == origin;
}
if !closed {
return Ok(false); }
cycles.push(cycle);
}
if cycles.len() != 2 {
return Ok(false);
}
let wrap_pi = |d: f64| -> f64 { (d + TAU / 2.0).rem_euclid(TAU) - TAU / 2.0 };
for cycle in &cycles {
let mut winding = 0.0_f64;
let mut whole_turn = false;
let mut at: Option<brepkit_topology::vertex::VertexId> = None;
for &ci in cycle {
let (_, sv, ev) = curved[ci];
if sv == ev {
whole_turn = true;
continue;
}
let (from, to) = match at {
None => (sv, ev),
Some(v) if v == sv => (sv, ev),
Some(_) => (ev, sv),
};
let (u0, _) = project(topo.vertex(from)?.point());
let (u1, _) = project(topo.vertex(to)?.point());
winding += wrap_pi(u1 - u0);
at = Some(to);
}
if !whole_turn && (winding.abs() - TAU).abs() > 1e-6 {
return Ok(false);
}
}
let mut rims: Vec<Vec<u32>> = Vec::with_capacity(2);
for cycle in &cycles {
let mut ids: Vec<u32> = Vec::new();
for &ci in cycle {
let Some(edge_ids) = edge_global_indices.get(&curved[ci].0) else {
return Ok(false);
};
ids.extend_from_slice(edge_ids);
}
ids.sort_unstable();
ids.dedup();
if ids.len() < 3 {
return Ok(false);
}
rims.push(ids);
}
let n = rims[0].len();
let angle_of = |gid: u32, merged: &TriangleMesh| project(merged.positions[gid as usize]).0;
for rim in &mut rims {
rim.sort_by(|&a, &b| {
angle_of(a, merged)
.partial_cmp(&angle_of(b, merged))
.unwrap_or(std::cmp::Ordering::Equal)
});
}
let emit = |merged: &mut TriangleMesh, a: u32, b: u32, c: u32| {
let (pa, pb, pc) = (
merged.positions[a as usize],
merged.positions[b as usize],
merged.positions[c as usize],
);
let geo = (pb - pa).cross(pc - pa);
if geo.length() < 1e-20 {
return;
}
let (u, v) = project(pa);
let outward = surf_normal(u, v);
let mut tri = [a, b, c];
if geo.dot(outward) < 0.0 {
tri.swap(1, 2);
}
merged.indices.extend_from_slice(&tri);
};
let m = rims[1].len();
if n == m {
for i in 0..n {
let j = (i + 1) % n;
let (b0, b1) = (rims[0][i], rims[0][j]);
let (t0, t1) = (rims[1][i], rims[1][j]);
emit(merged, b0, b1, t1);
emit(merged, b0, t1, t0);
}
return Ok(true);
}
let ang0: Vec<f64> = rims[0].iter().map(|&g| angle_of(g, merged)).collect();
let start1 = (0..m)
.min_by(|&a, &b| {
let ka = (angle_of(rims[1][a], merged) - ang0[0]).rem_euclid(TAU);
let kb = (angle_of(rims[1][b], merged) - ang0[0]).rem_euclid(TAU);
ka.partial_cmp(&kb).unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(0);
rims[1].rotate_left(start1);
let base = ang0[0];
let unwrap = |a: f64| (a - base).rem_euclid(TAU);
let a0: Vec<f64> = rims[0]
.iter()
.map(|&g| unwrap(angle_of(g, merged)))
.collect();
let a1: Vec<f64> = rims[1]
.iter()
.map(|&g| unwrap(angle_of(g, merged)))
.collect();
let (mut i, mut j) = (0usize, 0usize);
let (mut done0, mut done1) = (0usize, 0usize);
while done0 < n || done1 < m {
let next0 = if done0 >= n {
f64::INFINITY
} else if i + 1 < n {
a0[i + 1]
} else {
a0[0] + TAU
};
let next1 = if done1 >= m {
f64::INFINITY
} else if j + 1 < m {
a1[j + 1]
} else {
a1[0] + TAU
};
if next0 <= next1 {
let ni = (i + 1) % n;
emit(merged, rims[0][i], rims[1][j], rims[0][ni]);
i = ni;
done0 += 1;
} else {
let nj = (j + 1) % m;
emit(merged, rims[0][i], rims[1][j], rims[1][nj]);
j = nj;
done1 += 1;
}
}
Ok(true)
}
pub(super) fn tessellate_torus_two_rim_band(
topo: &Topology,
face_data: &brepkit_topology::face::Face,
deflection: f64,
angular_tol: f64,
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &mut TriangleMesh,
point_to_global: &mut DetHashMap<(i64, i64, i64), u32>,
) -> Result<bool, crate::OperationsError> {
use std::f64::consts::TAU;
let FaceSurface::Torus(torus) = face_data.surface() else {
return Ok(false);
};
if !face_data.inner_wires().is_empty() {
return Ok(false);
}
let wire = topo.wire(face_data.outer_wire())?;
let mut rim_edge_ids: Vec<usize> = Vec::new();
let mut seam: Option<(brepkit_topology::edge::EdgeId, usize)> = None;
for oe in wire.edges() {
let e = topo.edge(oe.edge())?;
let closed = e.start() == e.end();
match e.curve() {
EdgeCurve::Circle(_) if closed => {
let idx = oe.edge().index();
if !rim_edge_ids.contains(&idx) {
rim_edge_ids.push(idx);
}
}
EdgeCurve::Circle(_) | EdgeCurve::NurbsCurve(_) | EdgeCurve::Line if !closed => {
match &mut seam {
None => seam = Some((oe.edge(), 1)),
Some((eid, uses)) if *eid == oe.edge() => *uses += 1,
Some(_) => return Ok(false),
}
}
EdgeCurve::Circle(_)
| EdgeCurve::NurbsCurve(_)
| EdgeCurve::Line
| EdgeCurve::Ellipse(_) => return Ok(false),
}
}
let Some((seam_eid, 2)) = seam else {
return Ok(false);
};
if rim_edge_ids.len() != 2 {
return Ok(false);
}
let (t1, t2, t3) = (torus.clone(), torus.clone(), torus.clone());
let project = move |p: Point3| t1.project_point(p);
let surf_eval = move |u: f64, v: f64| t2.evaluate(u, v);
let surf_normal = move |u: f64, v: f64| t3.normal(u, v);
let circ_mean_spread = |angles: &[f64]| -> (f64, f64) {
let (mut sx, mut sy) = (0.0_f64, 0.0_f64);
for &a in angles {
sx += a.cos();
sy += a.sin();
}
let mean = sy.atan2(sx);
let spread = angles
.iter()
.map(|&a| {
let d = (a - mean + std::f64::consts::PI).rem_euclid(TAU) - std::f64::consts::PI;
d.abs()
})
.fold(0.0_f64, f64::max);
(mean.rem_euclid(TAU), spread)
};
let mut raw: Vec<Vec<(f64, f64, u32)>> = Vec::with_capacity(2);
for &re in &rim_edge_ids {
let Some(gids) = edge_global_indices.get(&re) else {
return Ok(false);
};
let mut seen: DetHashSet<u32> = DetHashSet::default();
let mut pts: Vec<(f64, f64, u32)> = Vec::with_capacity(gids.len());
for &g in gids {
if !seen.insert(g) {
continue;
}
let (u, v) = project(merged.positions[g as usize]);
pts.push((u, v, g));
}
if pts.len() < 3 {
return Ok(false);
}
raw.push(pts);
}
let spread_of = |pts: &[(f64, f64, u32)], pick_u: bool| -> (f64, f64) {
let angles: Vec<f64> = pts
.iter()
.map(|&(u, v, _)| if pick_u { u } else { v })
.collect();
circ_mean_spread(&angles)
};
let (u_stats0, v_stats0) = (spread_of(&raw[0], true), spread_of(&raw[0], false));
let (u_stats1, v_stats1) = (spread_of(&raw[1], true), spread_of(&raw[1], false));
let lat_mode = if v_stats0.1 <= 1e-6 && v_stats1.1 <= 1e-6 {
true
} else if u_stats0.1 <= 1e-6 && u_stats1.1 <= 1e-6 {
false
} else {
return Ok(false);
};
let (lvl0, lvl1) = if lat_mode {
(v_stats0.0, v_stats1.0)
} else {
(u_stats0.0, u_stats1.0)
};
let mut rims: Vec<LatRing> = Vec::with_capacity(2);
for pts in &raw {
let mut ring: LatRing = pts
.iter()
.map(|&(u, v, g)| if lat_mode { (u, g) } else { (v, g) })
.collect();
ring.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let max_gap = ring
.windows(2)
.map(|w| w[1].0 - w[0].0)
.chain(std::iter::once(ring[0].0 + TAU - ring[ring.len() - 1].0))
.fold(0.0_f64, f64::max);
if max_gap > std::f64::consts::PI {
return Ok(false);
}
rims.push(ring);
}
let seam_edge = topo.edge(seam_eid)?;
let sp = topo.vertex(seam_edge.start())?.point();
let ep = topo.vertex(seam_edge.end())?.point();
let (d0, d1) = seam_edge.curve().domain_with_endpoints(sp, ep);
let seam_mid = seam_edge
.curve()
.evaluate_with_endpoints(f64::midpoint(d0, d1), sp, ep);
let (mid_u, mid_v) = project(seam_mid);
let mid = if lat_mode { mid_v } else { mid_u };
let fwd_span = (lvl1 - lvl0).rem_euclid(TAU);
if fwd_span < 1e-9 || (TAU - fwd_span) < 1e-9 {
return Ok(false);
}
let mid_off = (mid - lvl0).rem_euclid(TAU);
let sweep = if mid_off <= fwd_span {
fwd_span
} else {
-(TAU - fwd_span)
};
let (sweep_radius, wrap_radius) = if lat_mode {
(
torus.minor_radius(),
torus.major_radius() + torus.minor_radius(),
)
} else {
(
torus.major_radius() + torus.minor_radius(),
torus.minor_radius(),
)
};
let n_rows =
segments_for_chord_deviation_a(sweep_radius, sweep.abs(), deflection, angular_tol, true)
.max(1);
let full_circle_cols =
segments_for_chord_deviation_a(wrap_radius, TAU, deflection, angular_tol, true);
let n_cols = rims[0].len().max(rims[1].len()).max(full_circle_cols);
let emit = make_band_emit(&project, &surf_normal);
let mut prev_ring: LatRing = rims[0].clone();
for i in 1..n_rows {
#[allow(clippy::cast_precision_loss)]
let t = i as f64 / n_rows as f64;
let level = lvl0 + sweep * t;
let mut row: LatRing = Vec::with_capacity(n_cols);
for j in 0..n_cols {
#[allow(clippy::cast_precision_loss)]
let a = TAU * (j as f64) / (n_cols as f64);
let (u, v) = if lat_mode { (a, level) } else { (level, a) };
let p = surf_eval(u, v);
let key = point_merge_key(p, MERGE_GRID);
let gid = *point_to_global.entry(key).or_insert_with(|| {
#[allow(clippy::cast_possible_truncation)]
let idx = merged.positions.len() as u32;
merged.positions.push(p);
merged.normals.push(surf_normal(u, v));
idx
});
row.push((a, gid));
}
stitch_rings(merged, &prev_ring, &row, &emit);
prev_ring = row;
}
stitch_rings(merged, &prev_ring, &rims[1], &emit);
Ok(true)
}
type LatRing = Vec<(f64, u32)>;
fn collect_torus_phi_ring(
topo: &Topology,
wire_id: brepkit_topology::wire::WireId,
torus: &brepkit_math::surfaces::ToroidalSurface,
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &TriangleMesh,
) -> Result<Option<Vec<(f64, u32)>>, crate::OperationsError> {
let wire = topo.wire(wire_id)?;
let mut gids: Vec<u32> = Vec::new();
for oe in wire.edges() {
let Some(edge_gids) = edge_global_indices.get(&oe.edge().index()) else {
return Ok(None);
};
gids.extend_from_slice(edge_gids);
}
let mut seen: DetHashSet<u32> = DetHashSet::default();
let mut ring: Vec<(f64, u32)> = Vec::with_capacity(gids.len());
for g in gids {
if !seen.insert(g) {
continue;
}
let (_, v) = torus.project_point(merged.positions[g as usize]);
ring.push((v, g));
}
if ring.len() < 3 {
return Ok(None);
}
ring.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let max_gap = ring
.windows(2)
.map(|w| w[1].0 - w[0].0)
.chain(std::iter::once(
ring[0].0 + std::f64::consts::TAU - ring[ring.len() - 1].0,
))
.fold(0.0_f64, f64::max);
if max_gap > std::f64::consts::PI {
return Ok(None);
}
Ok(Some(ring))
}
pub(super) fn tessellate_torus_notch_band(
topo: &Topology,
face_data: &brepkit_topology::face::Face,
deflection: f64,
angular_tol: f64,
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &mut TriangleMesh,
point_to_global: &mut DetHashMap<(i64, i64, i64), u32>,
) -> Result<bool, crate::OperationsError> {
use std::f64::consts::{PI, TAU};
let FaceSurface::Torus(torus) = face_data.surface() else {
return Ok(false);
};
if face_data.inner_wires().len() != 1 {
return Ok(false);
}
let t1 = torus.clone();
let t2 = torus.clone();
let project = move |p: Point3| t1.project_point(p);
let surf_normal = move |u: f64, v: f64| t2.normal(u, v);
let Some(ring_a) = collect_torus_phi_ring(
topo,
face_data.outer_wire(),
torus,
edge_global_indices,
merged,
)?
else {
return Ok(false);
};
let Some(ring_b) = collect_torus_phi_ring(
topo,
face_data.inner_wires()[0],
torus,
edge_global_indices,
merged,
)?
else {
return Ok(false);
};
let mean_u = |ring: &[(f64, u32)]| -> f64 {
let (mut sx, mut sy) = (0.0, 0.0);
for &(_, g) in ring {
let (u, _) = project(merged.positions[g as usize]);
sx += u.cos();
sy += u.sin();
}
sy.atan2(sx).rem_euclid(TAU)
};
let half_spread = |ring: &[(f64, u32)], mean: f64| -> f64 {
ring.iter()
.map(|&(_, g)| {
let (u, _) = project(merged.positions[g as usize]);
let d = (u - mean + PI).rem_euclid(TAU) - PI;
d.abs()
})
.fold(0.0_f64, f64::max)
};
let u_a = mean_u(&ring_a);
let u_b = mean_u(&ring_b);
let spread_a = half_spread(&ring_a, u_a);
let spread_b = half_spread(&ring_b, u_b);
let fwd_span = (u_b - u_a).rem_euclid(TAU); let (u_start, u_end) = if fwd_span >= PI {
(u_a + spread_a, u_a + fwd_span - spread_b)
} else {
(u_a - spread_a, u_a - (TAU - fwd_span) + spread_b)
};
let span = (u_end - u_start).abs();
if span < 1e-6 {
return Ok(false);
}
let n_u =
segments_for_chord_deviation_a(torus.major_radius(), span, deflection, angular_tol, true)
.max(2);
let n_v =
segments_for_chord_deviation_a(torus.minor_radius(), TAU, deflection, angular_tol, true)
.max(8);
let build_u_ring = |u: f64,
merged: &mut TriangleMesh,
point_to_global: &mut DetHashMap<(i64, i64, i64), u32>|
-> LatRing {
let mut row: LatRing = Vec::with_capacity(n_v);
for j in 0..n_v {
#[allow(clippy::cast_precision_loss)]
let v = TAU * (j as f64) / (n_v as f64);
let p = torus.evaluate(u, v);
let key = point_merge_key(p, MERGE_GRID);
let gid = *point_to_global.entry(key).or_insert_with(|| {
let idx = merged.positions.len() as u32;
merged.positions.push(p);
merged.normals.push(surf_normal(u, v));
idx
});
row.push((v, gid));
}
row.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
row
};
let emit = make_band_emit(&project, &surf_normal);
let idx_start = merged.indices.len();
let mut prev: LatRing = ring_a;
for iu in 1..n_u {
#[allow(clippy::cast_precision_loss)]
let u = u_start + (u_end - u_start) * (iu as f64) / (n_u as f64);
let row = build_u_ring(u.rem_euclid(TAU), merged, point_to_global);
stitch_rings(merged, &prev, &row, &emit);
prev = row;
}
stitch_rings(merged, &prev, &ring_b, &emit);
orient_triangle_run(merged, idx_start, &project, &surf_normal);
Ok(true)
}
#[allow(clippy::too_many_lines)]
pub(super) fn tessellate_latitude_band_shared(
topo: &Topology,
face_data: &brepkit_topology::face::Face,
deflection: f64,
angular_tol: f64,
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &mut TriangleMesh,
point_to_global: &mut DetHashMap<(i64, i64, i64), u32>,
) -> Result<bool, crate::OperationsError> {
if face_data.inner_wires().len() != 1 {
return Ok(false);
}
let (project, surf_eval, surf_normal): (ProjectFn, EvalFn, NormalFn) = match face_data.surface()
{
FaceSurface::Sphere(s) => {
let (s1, s2, s3) = (s.clone(), s.clone(), s.clone());
(
Box::new(move |p| s1.project_point(p)),
Box::new(move |u, v| s2.evaluate(u, v)),
Box::new(move |u, v| s3.normal(u, v)),
)
}
FaceSurface::Torus(t) => {
let (t1, t2, t3) = (t.clone(), t.clone(), t.clone());
(
Box::new(move |p| t1.project_point(p)),
Box::new(move |u, v| t2.evaluate(u, v)),
Box::new(move |u, v| t3.normal(u, v)),
)
}
_ => return Ok(false),
};
let band_radius = match face_data.surface() {
FaceSurface::Sphere(s) => s.radius(),
FaceSurface::Torus(t) => t.minor_radius(),
_ => return Ok(false),
};
let emit = make_band_emit(project.as_ref(), surf_normal.as_ref());
let full_circle_cols = segments_for_chord_deviation_a(
band_radius,
std::f64::consts::TAU,
deflection,
angular_tol,
true,
);
let outer_wid = face_data.outer_wire();
let inner_wid = face_data.inner_wires()[0];
let outer_const = collect_constant_v_ring(
topo,
outer_wid,
project.as_ref(),
edge_global_indices,
merged,
)?;
let inner_const = collect_constant_v_ring(
topo,
inner_wid,
project.as_ref(),
edge_global_indices,
merged,
)?;
if let (Some((v_outer, ring_outer)), Some((v_inner, ring_inner))) = (&outer_const, &inner_const)
{
let mut rings = [
(*v_outer, ring_outer.clone()),
(*v_inner, ring_inner.clone()),
];
rings.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let (v_lo, ring_lo) = (&rings[0].0, &rings[0].1);
let (v_hi, ring_hi) = (&rings[1].0, &rings[1].1);
let (v_lo, v_hi) = (*v_lo, *v_hi);
if (v_hi - v_lo).abs() < 1e-9 {
return Ok(false);
}
let n_v =
segments_for_chord_deviation_a(band_radius, v_hi - v_lo, deflection, angular_tol, true)
.max(1);
let n_u_interior = ring_lo.len().max(ring_hi.len()).max(full_circle_cols);
let mut prev_ring: LatRing = ring_lo.clone();
for iv in 1..n_v {
#[allow(clippy::cast_precision_loss)]
let t = iv as f64 / n_v as f64;
let v = v_lo + (v_hi - v_lo) * t;
let row = build_interior_row(
v,
n_u_interior,
surf_eval.as_ref(),
surf_normal.as_ref(),
merged,
point_to_global,
);
stitch_rings(merged, &prev_ring, &row, &emit);
prev_ring = row;
}
stitch_rings(merged, &prev_ring, ring_hi, &emit);
return Ok(true);
}
let Some((v_cap, cap_ring)) = inner_const else {
return Ok(false);
};
let Some(floor) = collect_var_v_ring(
topo,
outer_wid,
project.as_ref(),
edge_global_indices,
merged,
)?
else {
return Ok(false);
};
let floor_v_min = floor.iter().map(|r| r.1).fold(f64::INFINITY, f64::min);
let floor_v_max = floor.iter().map(|r| r.1).fold(f64::NEG_INFINITY, f64::max);
if (floor_v_max - floor_v_min) <= 1e-6 {
return Ok(false); }
let floor_v_near = if (v_cap - floor_v_max).abs() >= (v_cap - floor_v_min).abs() {
floor_v_max
} else {
floor_v_min
};
if (v_cap - floor_v_near).abs() < 1e-9 {
return Ok(false);
}
let n_v = segments_for_chord_deviation_a(
band_radius,
(v_cap - floor_v_near).abs(),
deflection,
angular_tol,
true,
)
.max(1);
let floor_ring: LatRing = floor.iter().map(|&(u, _, g)| (u, g)).collect();
let collar_idx_start = merged.indices.len();
let emit_raw = |merged: &mut TriangleMesh, a: u32, b: u32, c: u32| {
if a == b || b == c || a == c {
return;
}
let (pa, pb, pc) = (
merged.positions[a as usize],
merged.positions[b as usize],
merged.positions[c as usize],
);
if (pb - pa).cross(pc - pa).length() < 1e-20 {
return;
}
merged.indices.extend_from_slice(&[a, b, c]);
};
let mut prev_ring: LatRing = floor_ring;
for iv in 1..n_v {
#[allow(clippy::cast_precision_loss)]
let t = iv as f64 / n_v as f64;
let row = build_collar_row(
&floor,
v_cap,
t,
surf_eval.as_ref(),
surf_normal.as_ref(),
merged,
point_to_global,
);
emit_aligned_quad_strip(merged, &prev_ring, &row, &emit_raw);
prev_ring = row;
}
stitch_rings(merged, &prev_ring, &cap_ring, &emit_raw);
orient_triangle_run(
merged,
collar_idx_start,
project.as_ref(),
surf_normal.as_ref(),
);
Ok(true)
}
fn orient_triangle_run(
merged: &mut TriangleMesh,
idx_start: usize,
project: &dyn Fn(Point3) -> (f64, f64),
surf_normal: &dyn Fn(f64, f64) -> Vec3,
) {
let mut best_area = 0.0_f64;
let mut flip = false;
let mut t = idx_start;
while t + 3 <= merged.indices.len() {
let (a, b, c) = (
merged.indices[t],
merged.indices[t + 1],
merged.indices[t + 2],
);
let (pa, pb, pc) = (
merged.positions[a as usize],
merged.positions[b as usize],
merged.positions[c as usize],
);
let geo = (pb - pa).cross(pc - pa);
let area = geo.length();
if area > best_area {
best_area = area;
let centroid = Point3::new(
(pa.x() + pb.x() + pc.x()) / 3.0,
(pa.y() + pb.y() + pc.y()) / 3.0,
(pa.z() + pb.z() + pc.z()) / 3.0,
);
let (u, v) = project(centroid);
flip = geo.dot(surf_normal(u, v)) < 0.0;
}
t += 3;
}
if flip {
let mut t = idx_start;
while t + 3 <= merged.indices.len() {
merged.indices.swap(t + 1, t + 2);
t += 3;
}
}
}
fn emit_aligned_quad_strip(
merged: &mut TriangleMesh,
lo: &LatRing,
hi: &LatRing,
emit: &impl Fn(&mut TriangleMesh, u32, u32, u32),
) {
let n = lo.len();
if n < 2 || hi.len() != n {
stitch_rings(merged, lo, hi, emit);
return;
}
for i in 0..n {
let j = (i + 1) % n;
let (l0, l1) = (lo[i].1, lo[j].1);
let (h0, h1) = (hi[i].1, hi[j].1);
emit(merged, l0, l1, h1);
emit(merged, l0, h1, h0);
}
}
fn collect_constant_v_ring(
topo: &Topology,
wire_id: brepkit_topology::wire::WireId,
project: &dyn Fn(Point3) -> (f64, f64),
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &TriangleMesh,
) -> Result<Option<(f64, LatRing)>, crate::OperationsError> {
let wire = topo.wire(wire_id)?;
let mut gids: Vec<u32> = Vec::new();
for oe in wire.edges() {
let e = topo.edge(oe.edge())?;
match e.curve() {
EdgeCurve::Line | EdgeCurve::Circle(_) => {}
_ => return Ok(None),
}
let Some(edge_gids) = edge_global_indices.get(&oe.edge().index()) else {
return Ok(None);
};
for &g in edge_gids {
gids.push(g);
}
}
if gids.len() < 3 {
return Ok(None);
}
let mut seen: DetHashSet<u32> = DetHashSet::default();
let mut ring: LatRing = Vec::with_capacity(gids.len());
let mut v_sum = 0.0;
let mut v_min = f64::INFINITY;
let mut v_max = f64::NEG_INFINITY;
for g in gids {
if !seen.insert(g) {
continue;
}
let p = merged.positions[g as usize];
let (u, v) = project(p);
v_sum += v;
v_min = v_min.min(v);
v_max = v_max.max(v);
ring.push((u, g));
}
if ring.len() < 3 {
return Ok(None);
}
if (v_max - v_min) > 1e-6 {
return Ok(None);
}
ring.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let max_gap = ring
.windows(2)
.map(|w| w[1].0 - w[0].0)
.chain(std::iter::once(
ring[0].0 + std::f64::consts::TAU - ring[ring.len() - 1].0,
))
.fold(0.0_f64, f64::max);
if max_gap > std::f64::consts::PI {
return Ok(None);
}
let v_level = v_sum / ring.len() as f64;
Ok(Some((v_level, ring)))
}
type VarRing = Vec<(f64, f64, u32)>;
fn collect_var_v_ring(
topo: &Topology,
wire_id: brepkit_topology::wire::WireId,
project: &dyn Fn(Point3) -> (f64, f64),
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &TriangleMesh,
) -> Result<Option<VarRing>, crate::OperationsError> {
let wire = topo.wire(wire_id)?;
let mut gids: Vec<u32> = Vec::new();
for oe in wire.edges() {
let e = topo.edge(oe.edge())?;
match e.curve() {
EdgeCurve::Line | EdgeCurve::Circle(_) => {}
_ => return Ok(None),
}
let Some(edge_gids) = edge_global_indices.get(&oe.edge().index()) else {
return Ok(None);
};
gids.extend_from_slice(edge_gids);
}
if gids.len() < 3 {
return Ok(None);
}
let mut seen: DetHashSet<u32> = DetHashSet::default();
let mut ring: VarRing = Vec::with_capacity(gids.len());
for g in gids {
if !seen.insert(g) {
continue;
}
let (u, v) = project(merged.positions[g as usize]);
ring.push((u, v, g));
}
if ring.len() < 3 {
return Ok(None);
}
ring.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let max_gap = ring
.windows(2)
.map(|w| w[1].0 - w[0].0)
.chain(std::iter::once(
ring[0].0 + std::f64::consts::TAU - ring[ring.len() - 1].0,
))
.fold(0.0_f64, f64::max);
if max_gap > std::f64::consts::PI {
return Ok(None);
}
Ok(Some(ring))
}
fn build_interior_row(
v: f64,
n: usize,
surf_eval: &dyn Fn(f64, f64) -> Point3,
surf_normal: &dyn Fn(f64, f64) -> Vec3,
merged: &mut TriangleMesh,
point_to_global: &mut DetHashMap<(i64, i64, i64), u32>,
) -> LatRing {
let mut row: LatRing = Vec::with_capacity(n);
for i in 0..n {
let u = std::f64::consts::TAU * (i as f64) / (n as f64);
let p = surf_eval(u, v);
let key = point_merge_key(p, MERGE_GRID);
let gid = *point_to_global.entry(key).or_insert_with(|| {
let idx = merged.positions.len() as u32;
merged.positions.push(p);
merged.normals.push(surf_normal(u, v));
idx
});
row.push((u, gid));
}
row
}
fn build_collar_row(
floor: &VarRing,
v_cap: f64,
t: f64,
surf_eval: &dyn Fn(f64, f64) -> Point3,
surf_normal: &dyn Fn(f64, f64) -> Vec3,
merged: &mut TriangleMesh,
point_to_global: &mut DetHashMap<(i64, i64, i64), u32>,
) -> LatRing {
let mut row: LatRing = Vec::with_capacity(floor.len());
for &(u, v_floor, _) in floor {
let v = v_floor + (v_cap - v_floor) * t;
let p = surf_eval(u, v);
let key = point_merge_key(p, MERGE_GRID);
let gid = *point_to_global.entry(key).or_insert_with(|| {
let idx = merged.positions.len() as u32;
merged.positions.push(p);
merged.normals.push(surf_normal(u, v));
idx
});
row.push((u, gid));
}
row
}
fn make_band_emit<'a>(
project: &'a dyn Fn(Point3) -> (f64, f64),
surf_normal: &'a dyn Fn(f64, f64) -> Vec3,
) -> impl Fn(&mut TriangleMesh, u32, u32, u32) + 'a {
move |merged: &mut TriangleMesh, a: u32, b: u32, c: u32| {
if a == b || b == c || a == c {
return;
}
let (pa, pb, pc) = (
merged.positions[a as usize],
merged.positions[b as usize],
merged.positions[c as usize],
);
let geo = (pb - pa).cross(pc - pa);
if geo.length() < 1e-20 {
return;
}
let n_at = |p: Point3| -> Vec3 {
let (u, v) = project(p);
surf_normal(u, v)
};
let outward = n_at(pa) + n_at(pb) + n_at(pc);
let mut tri = [a, b, c];
if geo.dot(outward) < 0.0 {
tri.swap(1, 2);
}
merged.indices.extend_from_slice(&tri);
}
}
fn stitch_rings(
merged: &mut TriangleMesh,
lo: &LatRing,
hi: &LatRing,
emit: &impl Fn(&mut TriangleMesh, u32, u32, u32),
) {
if lo.len() < 2 || hi.len() < 2 {
return;
}
let (nl, nh) = (lo.len(), hi.len());
let unwrap_ring = |ring: &LatRing| -> Vec<f64> {
let mut acc = Vec::with_capacity(ring.len() + 1);
let mut prev = ring[0].0;
acc.push(prev);
for k in 1..=ring.len() {
let raw = ring[k % ring.len()].0;
let mut gap = (raw - prev).rem_euclid(std::f64::consts::TAU);
if gap <= 0.0 {
gap = std::f64::consts::TAU;
}
prev += gap;
acc.push(prev);
}
acc
};
let lo_ang = unwrap_ring(lo);
let hi_ang = unwrap_ring(hi);
let (mut i, mut j) = (0usize, 0usize);
for _ in 0..(nl + nh) {
let li = lo[i % nl].1;
let hj = hi[j % nh].1;
let lo_next = if i < nl { lo_ang[i + 1] } else { f64::INFINITY };
let hi_next = if j < nh { hi_ang[j + 1] } else { f64::INFINITY };
if lo_next <= hi_next {
let li_next = lo[(i + 1) % nl].1;
emit(merged, li, li_next, hj);
i += 1;
} else {
let hj_next = hi[(j + 1) % nh].1;
emit(merged, li, hj_next, hj);
j += 1;
}
}
}
fn cdt_trace() -> bool {
static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*TRACE.get_or_init(|| std::env::var("BK_CDT_TRACE").is_ok())
}
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
pub(super) fn tessellate_nonplanar_cdt(
topo: &Topology,
face_id: FaceId,
face_data: &brepkit_topology::face::Face,
deflection: f64,
angular_tol: f64,
circle_floor: bool,
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &mut TriangleMesh,
point_to_global: &mut DetHashMap<(i64, i64, i64), u32>,
) -> Result<(), crate::OperationsError> {
use brepkit_math::cdt::Cdt;
use brepkit_math::vec::Point2;
use brepkit_topology::edge::EdgeId;
let wire = topo.wire(face_data.outer_wire())?;
let tol_dup = 1e-10;
let mut boundary_3d: Vec<(Point3, u32, EdgeId, bool)> = Vec::new();
for oe in wire.edges() {
let edge_id_local = oe.edge();
let edge_idx = edge_id_local.index();
let is_fwd = oe.is_forward();
if let Some(global_ids) = edge_global_indices.get(&edge_idx) {
if cdt_trace() {
log::debug!(
"cdt {face_id:?} edge e{edge_idx} SHARED n={} gids {}..{}",
global_ids.len(),
global_ids.first().copied().unwrap_or(0),
global_ids.last().copied().unwrap_or(0)
);
}
let ordered: Vec<u32> = if is_fwd {
global_ids.clone()
} else {
global_ids.iter().rev().copied().collect()
};
for (j, &gid) in ordered.iter().enumerate() {
if j == 0 && !boundary_3d.is_empty() {
let (_, last_gid, _, _) = boundary_3d[boundary_3d.len() - 1];
if last_gid == gid
|| (merged.positions[last_gid as usize] - merged.positions[gid as usize])
.length()
< tol_dup
{
continue;
}
}
boundary_3d.push((merged.positions[gid as usize], gid, edge_id_local, is_fwd));
}
} else {
if cdt_trace() {
log::debug!("cdt {face_id:?} edge e{edge_idx} RESAMPLED");
}
let edge_data = topo.edge(oe.edge())?;
let points = sample_edge(topo, edge_data, deflection, angular_tol, circle_floor)?;
let ordered: Vec<Point3> = if is_fwd {
points
} else {
points.into_iter().rev().collect()
};
for (j, &pt) in ordered.iter().enumerate() {
if j == 0 && !boundary_3d.is_empty() {
let (last_pos, _, _, _) = boundary_3d[boundary_3d.len() - 1];
if (last_pos - pt).length() < tol_dup {
continue;
}
}
let key = point_merge_key(pt, MERGE_GRID);
let gid = *point_to_global.entry(key).or_insert_with(|| {
let idx = merged.positions.len() as u32;
merged.positions.push(pt);
merged.normals.push(Vec3::new(0.0, 0.0, 0.0));
idx
});
boundary_3d.push((pt, gid, edge_id_local, is_fwd));
}
}
}
if boundary_3d.len() > 2
&& let (Some(&(_, first_gid, _, _)), Some(&(_, last_gid, _, _))) =
(boundary_3d.first(), boundary_3d.last())
&& (first_gid == last_gid
|| (merged.positions[first_gid as usize] - merged.positions[last_gid as usize])
.length()
< tol_dup)
{
boundary_3d.pop();
}
let n_boundary = boundary_3d.len();
if n_boundary < 3 {
return Err(crate::OperationsError::InvalidInput {
reason: "non-planar face has fewer than 3 boundary vertices".to_string(),
});
}
let mut boundary_uv: Vec<(f64, f64)> = boundary_3d
.iter()
.map(|(pt, _, edge_id_local, _)| {
if let Some(pcurve) = topo.pcurves().get(*edge_id_local, face_id) {
let uv = project_via_pcurve(pcurve, *pt, face_data.surface());
if let Some(uv) = uv {
return Ok(uv);
}
}
project_to_surface_uv(face_data.surface(), *pt)
})
.collect::<Result<Vec<_>, _>>()?;
{
let is_periodic = matches!(
face_data.surface(),
FaceSurface::Cylinder(_)
| FaceSurface::Cone(_)
| FaceSurface::Sphere(_)
| FaceSurface::Torus(_)
);
if is_periodic && !boundary_uv.is_empty() {
let degenerate_u = |v: f64| -> bool {
if let FaceSurface::Torus(t) = face_data.surface() {
(t.major_radius() + t.minor_radius() * v.cos()).abs() < t.minor_radius() * 1e-6
} else {
false
}
};
let n = boundary_uv.len();
let start = (0..n)
.find(|&i| !degenerate_u(boundary_uv[i].1))
.unwrap_or(0);
for k in 1..n {
let i = (start + k) % n;
let prev = (start + k + n - 1) % n;
let prev_u = boundary_uv[prev].0;
if degenerate_u(boundary_uv[i].1) {
boundary_uv[i].0 = prev_u;
continue;
}
let mut u = boundary_uv[i].0;
let diff = u - prev_u;
let shifts = (diff / std::f64::consts::TAU + 0.5).floor();
u -= shifts * std::f64::consts::TAU;
boundary_uv[i].0 = u;
}
let first_u = boundary_uv[0].0;
let last_u = boundary_uv.last().map_or(first_u, |p| p.0);
let close_diff = first_u - last_u;
if close_diff.abs() > std::f64::consts::PI {
let u_mid = boundary_uv.iter().map(|p| p.0).sum::<f64>() / boundary_uv.len() as f64;
let target_mid = std::f64::consts::PI;
let shift = target_mid - u_mid;
for pt in &mut boundary_uv {
pt.0 += shift;
}
}
}
if matches!(face_data.surface(), FaceSurface::Torus(_)) && !boundary_uv.is_empty() {
for i in 1..boundary_uv.len() {
let prev_v = boundary_uv[i - 1].1;
let mut v = boundary_uv[i].1;
let diff = v - prev_v;
let shifts = (diff / std::f64::consts::TAU + 0.5).floor();
v -= shifts * std::f64::consts::TAU;
boundary_uv[i].1 = v;
}
}
}
#[allow(clippy::items_after_statements)]
fn uv_bounds(uvs: &[(f64, f64)]) -> (f64, f64, f64, f64) {
uvs.iter().fold(
(
f64::INFINITY,
f64::NEG_INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
),
|(u_lo, u_hi, v_lo, v_hi), &(u, v)| {
(u_lo.min(u), u_hi.max(u), v_lo.min(v), v_hi.max(v))
},
)
}
let (u_min, u_max, v_min, v_max) = uv_bounds(&boundary_uv);
let (u_min, u_max, v_min, v_max) = {
let mut wire_edge_counts: DetHashMap<usize, usize> = DetHashMap::default();
for oe in wire.edges() {
*wire_edge_counts.entry(oe.edge().index()).or_default() += 1;
}
let seam_edge_indices: DetHashSet<usize> = wire_edge_counts
.iter()
.filter(|&(_, &c)| c > 1)
.map(|(&idx, _)| idx)
.collect();
if !seam_edge_indices.is_empty() {
let non_seam_uvs: Vec<(f64, f64)> = boundary_uv
.iter()
.enumerate()
.filter(|(i, _)| !seam_edge_indices.contains(&boundary_3d[*i].2.index()))
.map(|(_, &uv)| uv)
.collect();
let (u_min_bnd, u_max_bnd, v_min_bnd, v_max_bnd) = if non_seam_uvs.is_empty() {
(u_min, u_max, v_min, v_max)
} else {
uv_bounds(&non_seam_uvs)
};
#[allow(clippy::items_after_statements)]
struct SeamRun {
indices: Vec<usize>,
is_forward: bool,
}
let mut seam_runs: Vec<SeamRun> = Vec::new();
let mut current_indices: Vec<usize> = Vec::new();
let mut current_fwd: Option<bool> = None;
for i in 0..n_boundary {
let (_, _, edge_id, is_fwd) = boundary_3d[i];
if seam_edge_indices.contains(&edge_id.index()) {
current_indices.push(i);
if current_fwd.is_none() {
current_fwd = Some(is_fwd);
}
} else if !current_indices.is_empty() {
seam_runs.push(SeamRun {
indices: std::mem::take(&mut current_indices),
is_forward: current_fwd.unwrap_or(true),
});
current_fwd = None;
}
}
if !current_indices.is_empty() {
let tail_fwd = current_fwd.unwrap_or(true);
if !seam_runs.is_empty()
&& seam_edge_indices.contains(&boundary_3d[0].2.index())
&& seam_runs[0].is_forward == tail_fwd
{
current_indices.extend(seam_runs.remove(0).indices);
}
seam_runs.push(SeamRun {
indices: current_indices,
is_forward: tail_fwd,
});
}
for run in &seam_runs {
let u_assign = if run.is_forward { u_max_bnd } else { u_min_bnd };
let n_pts = run.indices.len();
let v_first = boundary_uv[run.indices[0]].1;
let (v_start, v_end) = if (v_first - v_min_bnd).abs() < (v_first - v_max_bnd).abs()
{
(v_min_bnd, v_max_bnd)
} else {
(v_max_bnd, v_min_bnd)
};
for (k, &i) in run.indices.iter().enumerate() {
let t = if n_pts > 1 {
k as f64 / (n_pts - 1) as f64
} else {
0.5
};
let v = v_start + t * (v_end - v_start);
boundary_uv[i] = (u_assign, v);
}
}
}
uv_bounds(&boundary_uv)
};
let margin = 0.01;
let bounds = (
Point2::new(u_min - margin, v_min - margin),
Point2::new(u_max + margin, v_max + margin),
);
let mut cdt = Cdt::with_capacity(bounds, n_boundary);
let mut cdt_to_global: Vec<Option<u32>> = vec![None; 3];
let boundary_pts: Vec<Point2> = boundary_uv
.iter()
.map(|&(u, v)| Point2::new(u, v))
.collect();
let boundary_cdt_ids = cdt
.insert_points_hilbert(&boundary_pts)
.map_err(crate::OperationsError::Math)?;
if cdt_trace() {
for (i, &cid) in boundary_cdt_ids.iter().enumerate() {
log::debug!(
"cdt {face_id:?} bpt[{i}] gid={} cdtid={cid} uv=({:.5},{:.5})",
boundary_3d[i].1,
boundary_uv[i].0,
boundary_uv[i].1
);
}
}
let max_cdt_idx = boundary_cdt_ids.iter().copied().max().unwrap_or(2);
if cdt_to_global.len() <= max_cdt_idx {
cdt_to_global.resize(max_cdt_idx + 1, None);
}
for (i, &cdt_idx) in boundary_cdt_ids.iter().enumerate() {
cdt_to_global[cdt_idx] = Some(boundary_3d[i].1);
}
for i in 0..n_boundary {
let v0 = boundary_cdt_ids[i];
let v1 = boundary_cdt_ids[(i + 1) % n_boundary];
cdt.insert_constraint(v0, v1)
.map_err(crate::OperationsError::Math)?;
}
let du = u_max - u_min;
let dv = v_max - v_min;
if du > 1e-15 && dv > 1e-15 {
let (n_u, n_v) = interior_grid_resolution(
face_data.surface(),
du,
dv,
deflection,
angular_tol,
circle_floor,
);
let boundary_uv_ref = &boundary_uv;
let interior_pts: Vec<Point2> = (1..n_u)
.flat_map(|iu| {
(1..n_v).filter_map(move |iv| {
let u = u_min + du * (iu as f64 / n_u as f64);
let v = v_min + dv * (iv as f64 / n_v as f64);
let pt2 = Point2::new(u, v);
point_in_polygon_2d(boundary_uv_ref, pt2).then_some(pt2)
})
})
.collect();
if !interior_pts.is_empty() {
let interior_cdt_ids = cdt
.insert_points_hilbert(&interior_pts)
.map_err(crate::OperationsError::Math)?;
let max_interior = interior_cdt_ids.iter().copied().max().unwrap_or(0);
if cdt_to_global.len() <= max_interior {
cdt_to_global.resize(max_interior + 1, None);
}
}
}
let boundary_pairs: Vec<(usize, usize)> = (0..n_boundary)
.map(|i| (boundary_cdt_ids[i], boundary_cdt_ids[(i + 1) % n_boundary]))
.collect();
cdt.remove_exterior(&boundary_pairs);
let cdt_verts = cdt.vertices();
let triangles = cdt.triangles();
if cdt_to_global.len() < cdt_verts.len() {
cdt_to_global.resize(cdt_verts.len(), None);
}
let mut final_global_ids: Vec<u32> = vec![0; cdt_to_global.len()];
for i in 0..cdt_to_global.len() {
if let Some(gid) = cdt_to_global[i] {
final_global_ids[i] = gid;
} else if i >= 3 {
let pt2 = cdt_verts[i];
let surface = face_data.surface();
let pt3 = eval_surface_point(surface, pt2.x(), pt2.y());
let nrm = surface.normal(pt2.x(), pt2.y());
let key = point_merge_key(pt3, MERGE_GRID);
let gid = *point_to_global.entry(key).or_insert_with(|| {
let idx = merged.positions.len() as u32;
merged.positions.push(pt3);
merged.normals.push(nrm);
idx
});
final_global_ids[i] = gid;
}
}
let mut vote = 0.0;
for &(i0, i1, i2) in &triangles {
if i0 < 3 || i1 < 3 || i2 < 3 {
continue;
}
let (p0, p1, p2) = (
merged.positions[final_global_ids[i0] as usize],
merged.positions[final_global_ids[i1] as usize],
merged.positions[final_global_ids[i2] as usize],
);
let geo = (p1 - p0).cross(p2 - p0);
let (uv0, uv1, uv2) = (cdt_verts[i0], cdt_verts[i1], cdt_verts[i2]);
let uc = (uv0.x() + uv1.x() + uv2.x()) / 3.0;
let vc = (uv0.y() + uv1.y() + uv2.y()) / 3.0;
let outward = face_data.surface().normal(uc, vc);
vote += geo.dot(outward);
}
let flip_all = vote < 0.0;
for (i0, i1, i2) in triangles {
if i0 < 3 || i1 < 3 || i2 < 3 {
continue; }
if flip_all {
merged.indices.push(final_global_ids[i0]);
merged.indices.push(final_global_ids[i2]);
merged.indices.push(final_global_ids[i1]);
} else {
merged.indices.push(final_global_ids[i0]);
merged.indices.push(final_global_ids[i1]);
merged.indices.push(final_global_ids[i2]);
}
}
Ok(())
}
fn project_to_surface_uv(
surface: &FaceSurface,
pt: Point3,
) -> Result<(f64, f64), crate::OperationsError> {
match surface {
FaceSurface::Cylinder(cyl) => Ok(cyl.project_point(pt)),
FaceSurface::Cone(cone) => Ok(cone.project_point(pt)),
FaceSurface::Sphere(sphere) => Ok(sphere.project_point(pt)),
FaceSurface::Torus(torus) => Ok(torus.project_point(pt)),
FaceSurface::Nurbs(surface) => {
brepkit_math::nurbs::projection::project_point_to_surface(surface, pt, 1e-6)
.map(|proj| (proj.u, proj.v))
.map_err(crate::OperationsError::Math)
}
FaceSurface::Plane { .. } => Err(crate::OperationsError::InvalidInput {
reason: "planar faces should not use CDT tessellation".to_string(),
}),
}
}
fn project_via_pcurve(
pcurve: &brepkit_topology::pcurve::PCurve,
pt: Point3,
surface: &FaceSurface,
) -> Option<(f64, f64)> {
let t_start = pcurve.t_start();
let t_end = pcurve.t_end();
let n_samples = 16;
let mut best_t = t_start;
let mut best_dist = f64::MAX;
for i in 0..=n_samples {
let t = t_start + (t_end - t_start) * (i as f64) / (n_samples as f64);
let uv = pcurve.evaluate(t);
let p_surf = eval_surface_point(surface, uv.x(), uv.y());
let d = (p_surf - pt).length();
if d < best_dist {
best_dist = d;
best_t = t;
}
}
let dt = (t_end - t_start) / (n_samples as f64);
let mut lo = (best_t - dt).max(t_start);
let mut hi = (best_t + dt).min(t_end);
for _ in 0..10 {
let mid = 0.5 * (lo + hi);
let uv_lo = pcurve.evaluate(lo);
let uv_hi = pcurve.evaluate(hi);
let d_lo = (eval_surface_point(surface, uv_lo.x(), uv_lo.y()) - pt).length();
let d_hi = (eval_surface_point(surface, uv_hi.x(), uv_hi.y()) - pt).length();
if d_lo < d_hi {
hi = mid;
} else {
lo = mid;
}
}
let t_final = 0.5 * (lo + hi);
let uv = pcurve.evaluate(t_final);
let p_final = eval_surface_point(surface, uv.x(), uv.y());
if (p_final - pt).length() < brepkit_math::tolerance::Tolerance::default().linear {
Some((uv.x(), uv.y()))
} else {
None
}
}
fn eval_surface_point(surface: &FaceSurface, u: f64, v: f64) -> Point3 {
surface.evaluate(u, v).unwrap_or(Point3::new(0.0, 0.0, 0.0))
}
fn estimate_surface_radius(surface: &FaceSurface) -> f64 {
match surface {
FaceSurface::Cylinder(cyl) => cyl.radius(),
FaceSurface::Cone(_) => 1.0,
FaceSurface::Sphere(sphere) => sphere.radius(),
FaceSurface::Torus(torus) => torus.major_radius() + torus.minor_radius(),
FaceSurface::Nurbs(_) | FaceSurface::Plane { .. } => 1.0,
}
}
fn interior_grid_resolution(
surface: &FaceSurface,
du: f64,
dv: f64,
deflection: f64,
angular_tol: f64,
circle_floor: bool,
) -> (usize, usize) {
match surface {
FaceSurface::Sphere(sphere) => {
let r = sphere.radius();
let n_u = segments_for_chord_deviation_a(r, du, deflection, angular_tol, true).max(2);
let n_v = segments_for_chord_deviation_a(r, dv, deflection, angular_tol, true).max(2);
(n_u, n_v)
}
FaceSurface::Torus(torus) => {
let n_u = segments_for_chord_deviation_a(
torus.major_radius(),
du,
deflection,
angular_tol,
true,
)
.max(2);
let n_v = segments_for_chord_deviation_a(
torus.minor_radius(),
dv,
deflection,
angular_tol,
true,
)
.max(2);
(n_u, n_v)
}
FaceSurface::Cylinder(_) | FaceSurface::Cone(_) => {
if !circle_floor {
return (2, 1);
}
let r = estimate_surface_radius(surface);
let n_u = segments_for_chord_deviation_a(r, du, deflection, angular_tol, true).max(2);
(n_u, 2)
}
FaceSurface::Plane { .. } | FaceSurface::Nurbs(_) => {
let r = estimate_surface_radius(surface);
let n_u = segments_for_chord_deviation_a(r, du, deflection, angular_tol, true).max(2);
let n_v = segments_for_chord_deviation_a(r, dv, deflection, angular_tol, true).max(2);
(n_u, n_v)
}
}
}
pub(super) fn point_in_polygon_2d(polygon: &[(f64, f64)], pt: brepkit_math::vec::Point2) -> bool {
let n = polygon.len();
let mut winding = 0i32;
for i in 0..n {
let j = (i + 1) % n;
let yi = polygon[i].1;
let yj = polygon[j].1;
if yi <= pt.y() {
if yj > pt.y() {
let cross = (polygon[j].0 - polygon[i].0) * (pt.y() - yi)
- (pt.x() - polygon[i].0) * (yj - yi);
if cross > 0.0 {
winding += 1;
}
}
} else if yj <= pt.y() {
let cross =
(polygon[j].0 - polygon[i].0) * (pt.y() - yi) - (pt.x() - polygon[i].0) * (yj - yi);
if cross < 0.0 {
winding -= 1;
}
}
}
winding != 0
}
#[allow(clippy::too_many_arguments)]
pub(super) fn tessellate_nonplanar_snap(
topo: &Topology,
face_id: FaceId,
face_data: &brepkit_topology::face::Face,
deflection: f64,
angular_tol: f64,
circle_floor: bool,
edge_global_indices: &DetHashMap<usize, Vec<u32>>,
merged: &mut TriangleMesh,
point_to_global: &mut DetHashMap<(i64, i64, i64), u32>,
) -> Result<(), crate::OperationsError> {
let mut face_mesh = super::face::tessellate_with_uvs_floor(
topo,
face_id,
deflection,
angular_tol,
circle_floor,
)
.map(|uv| uv.mesh)?;
if face_data.is_reversed() {
let tri_count = face_mesh.indices.len() / 3;
for t in 0..tri_count {
face_mesh.indices.swap(t * 3 + 1, t * 3 + 2);
}
for n in &mut face_mesh.normals {
*n = -*n;
}
}
let mut local_to_global: Vec<u32> = Vec::with_capacity(face_mesh.positions.len());
let wire = topo.wire(face_data.outer_wire())?;
let mut snap_targets: Vec<(Point3, u32)> = Vec::new();
for oe in wire.edges() {
if let Some(global_ids) = edge_global_indices.get(&oe.edge().index()) {
for &gid in global_ids {
if (gid as usize) < merged.positions.len() {
snap_targets.push((merged.positions[gid as usize], gid));
}
}
}
}
for &inner_wire_id in face_data.inner_wires() {
if let Ok(inner_wire) = topo.wire(inner_wire_id) {
for oe in inner_wire.edges() {
if let Some(global_ids) = edge_global_indices.get(&oe.edge().index()) {
for &gid in global_ids {
if (gid as usize) < merged.positions.len() {
snap_targets.push((merged.positions[gid as usize], gid));
}
}
}
}
}
}
let snap_tol = 1e-6;
let inv_cell = 1.0 / snap_tol;
let mut snap_grid: DetHashMap<(i64, i64, i64), Vec<u32>> =
DetHashMap::with_capacity_and_hasher(snap_targets.len(), brepkit_math::det_hash::DetState);
for &(target_pos, gid) in &snap_targets {
let cx = (target_pos.x() * inv_cell).round() as i64;
let cy = (target_pos.y() * inv_cell).round() as i64;
let cz = (target_pos.z() * inv_cell).round() as i64;
snap_grid.entry((cx, cy, cz)).or_default().push(gid);
}
for (i, &pos) in face_mesh.positions.iter().enumerate() {
let cx = (pos.x() * inv_cell).round() as i64;
let cy = (pos.y() * inv_cell).round() as i64;
let cz = (pos.z() * inv_cell).round() as i64;
let mut best_gid = None;
let mut best_dist = snap_tol;
for dx in -1_i64..=1 {
for dy in -1_i64..=1 {
for dz in -1_i64..=1 {
if let Some(gids) = snap_grid.get(&(cx + dx, cy + dy, cz + dz)) {
for &gid in gids {
let target_pos = merged.positions[gid as usize];
let dist = (pos - target_pos).length();
if dist < best_dist {
best_dist = dist;
best_gid = Some(gid);
}
}
}
}
}
}
if let Some(gid) = best_gid {
local_to_global.push(gid);
} else {
let key = point_merge_key(pos, MERGE_GRID);
let gid = point_to_global.entry(key).or_insert_with(|| {
let idx = merged.positions.len() as u32;
merged.positions.push(pos);
merged.normals.push(
face_mesh
.normals
.get(i)
.copied()
.unwrap_or(Vec3::new(0.0, 0.0, 1.0)),
);
idx
});
local_to_global.push(*gid);
}
}
for &li in &face_mesh.indices {
merged.indices.push(local_to_global[li as usize]);
}
Ok(())
}