use super::*;
use crate::sphere_chart::{
chart_grid_divisions, chart_grid_parameters, cube_edge_divisions, cube_edge_parameters,
ChartSide, SphereAtlas, SphericalRegion, CHART_COUNT, CUBE_CORNER_COUNT, CUBE_EDGE_COUNT,
};
const ON_EDGE_TOLERANCE: f64 = 1e-9;
const CONSTRAINT_CLEARANCE: f64 = 0.35;
const MAX_REFINE_PASSES: usize = 3;
struct AtlasVertices {
positions: Vec<Vec3>,
corners: [Option<usize>; CUBE_CORNER_COUNT],
}
impl AtlasVertices {
fn new() -> Self {
Self {
positions: Vec::new(),
corners: [None; CUBE_CORNER_COUNT],
}
}
fn push(&mut self, position: Vec3) -> usize {
self.positions.push(position);
self.positions.len() - 1
}
}
#[derive(Clone, Copy)]
struct EdgePoint {
q: f64,
vertex: usize,
}
struct ChartChain {
points: Vec<([f64; 2], usize)>,
}
fn decline(face_id: u32, why: &str) -> Result<bool, String> {
if std::env::var("BREP_DEBUG_SPHERE_CHARTS").is_ok() {
eprintln!("[sphere-atlas] face {face_id}: DECLINED — {why}");
}
Ok(false)
}
pub(super) fn tessellate_spherical_face_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_SPHERE_CHARTS").is_ok() {
return Ok(false);
}
let Some(atlas) = SphereAtlas::of_surface(&face.surface) else {
return Ok(false);
};
let outward = atlas.parameterization_is_outward(&face.surface)?;
let mut slit: HashMap<u64, usize> = HashMap::new();
for coedge in face.loops.iter().flat_map(|record| record.coedges.iter()) {
*slit.entry(coedge.edge_id).or_insert(0) += 1;
}
let mut boundary: Vec<(Vec3, Vec3)> = Vec::new();
for coedge in face.loops.iter().flat_map(|record| record.coedges.iter()) {
if slit.get(&coedge.edge_id).copied().unwrap_or(0) >= 2 {
continue;
}
let edge_samples = samples
.get(&coedge.edge_id)
.ok_or("sphere atlas: missing edge samples")?;
let count = edge_samples.positions.len();
for index in 0..count.saturating_sub(1) {
let (a, b) = if coedge.forward {
(edge_samples.positions[index], edge_samples.positions[index + 1])
} else {
(
edge_samples.positions[count - 1 - index],
edge_samples.positions[count - 2 - index],
)
};
boundary.push((a, b));
}
}
let outward_face_normal = face.same_sense == outward;
let region = SphericalRegion::from_segments(atlas.centre, &boundary, outward_face_normal);
if !region.is_decidable() {
return decline(face_id, "no seed: the region could not be classified");
}
let mut vertices = AtlasVertices::new();
let mut segments = Vec::new();
for &(a, b) in &boundary {
segments.extend(atlas.split_segment(a, b)?);
}
let mut trim_vertex: HashMap<[u64; 3], usize> = HashMap::new();
let mut edge_pinned: Vec<Vec<(f64, usize)>> = vec![Vec::new(); CUBE_EDGE_COUNT];
let key = |p: Vec3| [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
let name = |vertices: &mut AtlasVertices,
trim_vertex: &mut HashMap<[u64; 3], usize>,
edge_pinned: &mut Vec<Vec<(f64, usize)>>,
chart: usize,
point: Vec3|
-> usize {
if let Some(&index) = trim_vertex.get(&key(point)) {
return index;
}
let index = vertices.push(point);
trim_vertex.insert(key(point), index);
let sides = chart_sides(&atlas, chart, point);
for &(side, edge, q) in &sides {
let _ = side;
edge_pinned[edge].push((q, index));
}
if sides.len() >= 2 {
let (_, edge, q) = sides[0];
let corner = crate::sphere_chart::cube_edge_corner(edge, q > 0.0);
vertices.corners[corner] = Some(index);
}
index
};
let mut edge_spans: Vec<Vec<(f64, f64)>> = vec![Vec::new(); CUBE_EDGE_COUNT];
for segment in &segments {
name(&mut vertices, &mut trim_vertex, &mut edge_pinned, segment.chart, segment.start);
name(&mut vertices, &mut trim_vertex, &mut edge_pinned, segment.chart, segment.end);
if let Some((edge, a, b)) = shared_chart_side(&atlas, segment.chart, segment.start, segment.end)
{
edge_spans[edge].push((a.min(b), a.max(b)));
}
}
let divisions = cube_edge_divisions(atlas.radius, chord_tolerance);
let base = cube_edge_parameters(divisions);
let spacing = 2.0 / divisions as f64;
let mut edge_points: Vec<Vec<EdgePoint>> = Vec::with_capacity(CUBE_EDGE_COUNT);
for edge in 0..CUBE_EDGE_COUNT {
let mut pinned = edge_pinned[edge].clone();
pinned.sort_by(|a, b| a.0.total_cmp(&b.0));
pinned.dedup_by(|a, b| (a.0 - b.0).abs() <= 1e-12);
let mut list: Vec<EdgePoint> = Vec::with_capacity(base.len() + pinned.len());
for &q in &base {
if q.abs() >= 1.0 {
let corner = crate::sphere_chart::cube_edge_corner(edge, q > 0.0);
let vertex = match pinned
.iter()
.find(|(pin, _)| (pin - q).abs() <= 1e-9)
.map(|(_, vertex)| *vertex)
.or(vertices.corners[corner])
{
Some(existing) => existing,
None => {
let point = corner_point(&atlas, corner)?;
let index = vertices.push(point);
vertices.corners[corner] = Some(index);
index
}
};
list.push(EdgePoint { q, vertex });
continue;
}
if pinned
.iter()
.any(|(pin, _)| (pin - q).abs() <= 0.25 * spacing)
{
continue;
}
if edge_spans[edge]
.iter()
.any(|(low, high)| q > low + 1e-12 && q < high - 1e-12)
{
continue;
}
list.push(EdgePoint {
q,
vertex: vertices.push(atlas.edge_point(edge, q)?),
});
}
for &(q, vertex) in &pinned {
list.push(EdgePoint { q, vertex });
}
list.sort_by(|a, b| a.q.total_cmp(&b.q));
list.dedup_by(|a, b| a.vertex == b.vertex);
edge_points.push(list);
}
let mut blocking: HashSet<(usize, usize)> = HashSet::new();
let mut chains: Vec<Vec<ChartChain>> = (0..CHART_COUNT).map(|_| Vec::new()).collect();
for segment in &segments {
let start = trim_vertex[&key(segment.start)];
let end = trim_vertex[&key(segment.end)];
if start == end {
continue;
}
blocking.insert(ordered(start, end));
if shared_chart_side(&atlas, segment.chart, segment.start, segment.end)
.is_some_and(|(edge, _, _)| {
block_edge_span(&edge_points[edge], start, end, &mut blocking)
})
{
continue;
}
let chart = segment.chart;
let (Some(a), Some(b)) = (
chart_uv(&atlas, chart, segment.start),
chart_uv(&atlas, chart, segment.end),
) else {
return decline(face_id, "a trim segment fell outside the chart it was assigned");
};
chains[chart].push(ChartChain {
points: vec![(a, start), (b, end)],
});
}
let mut triangles: Vec<[usize; 3]> = Vec::new();
for chart in 0..CHART_COUNT {
let Some(chart_triangles) = mesh_chart(
face_id,
&atlas,
chart,
&edge_points,
&chains[chart],
&mut vertices,
chord_tolerance,
)?
else {
return decline(face_id, &format!("chart {chart} could not be triangulated"));
};
triangles.extend(chart_triangles);
}
if triangles.is_empty() {
return decline(face_id, "no triangles");
}
let keep = material_regions(&triangles, &blocking, &vertices, &atlas, ®ion);
if std::env::var("BREP_DEBUG_SPHERE_CHARTS").is_ok() {
let kept = keep.iter().filter(|value| **value).count();
eprintln!(
"[sphere-atlas] face {face_id}: boundary={} segments={} whole={} triangles={} kept={} outward={outward} same_sense={}",
boundary.len(),
segments.len(),
region.is_whole_sphere(),
triangles.len(),
kept,
face.same_sense,
);
}
let ccw = face.same_sense == outward;
let base_index = (mesh.positions.len() / 3) as u32;
let mut emitted: HashMap<usize, u32> = HashMap::new();
let mut used: Vec<u32> = Vec::new();
for (index, triangle) in triangles.iter().enumerate() {
if !keep[index] {
continue;
}
let mut corners = [0u32; 3];
for (slot, &vertex) in triangle.iter().enumerate() {
corners[slot] = match emitted.get(&vertex) {
Some(&existing) => existing,
None => {
let position = vertices.positions[vertex];
let mut normal = position.sub(atlas.centre).normalized()?;
if !ccw {
normal = normal.scale(-1.0);
}
mesh.positions.extend([position.x, position.y, position.z]);
mesh.normals.extend([normal.x, normal.y, normal.z]);
let new = base_index + used.len() as u32;
used.push(new);
emitted.insert(vertex, new);
new
}
};
}
if corners[0] == corners[1] || corners[1] == corners[2] || corners[2] == corners[0] {
continue;
}
if ccw {
mesh.indices.extend(corners);
} else {
mesh.indices.extend([corners[0], corners[2], corners[1]]);
}
mesh.face_ids.push(face_id);
}
Ok(!mesh.indices.is_empty())
}
fn ordered(a: usize, b: usize) -> (usize, usize) {
if a <= b {
(a, b)
} else {
(b, a)
}
}
fn corner_point(atlas: &SphereAtlas, corner: usize) -> Result<Vec3, String> {
let sign = |bit: usize| if corner & (1 << bit) != 0 { 1.0 } else { -1.0 };
let direction = atlas.basis[0]
.scale(sign(0))
.add(atlas.basis[1].scale(sign(1)))
.add(atlas.basis[2].scale(sign(2)))
.normalized()?;
Ok(atlas.centre.add(direction.scale(atlas.radius)))
}
fn chart_sides(atlas: &SphereAtlas, chart: usize, point: Vec3) -> Vec<(ChartSide, usize, f64)> {
let Some((s, t)) = atlas.coordinates(chart, point) else {
return Vec::new();
};
let mut sides = Vec::new();
let mut push = |side: ChartSide, free: f64| {
let (edge, sign) = side.edge(chart);
sides.push((side, edge, (sign * free).clamp(-1.0, 1.0)));
};
if s >= 1.0 - ON_EDGE_TOLERANCE {
push(ChartSide::SPlus, t);
} else if s <= -1.0 + ON_EDGE_TOLERANCE {
push(ChartSide::SMinus, t);
}
if t >= 1.0 - ON_EDGE_TOLERANCE {
push(ChartSide::TPlus, s);
} else if t <= -1.0 + ON_EDGE_TOLERANCE {
push(ChartSide::TMinus, s);
}
sides
}
fn shared_chart_side(
atlas: &SphereAtlas,
chart: usize,
a: Vec3,
b: Vec3,
) -> Option<(usize, f64, f64)> {
let start = chart_sides(atlas, chart, a);
let end = chart_sides(atlas, chart, b);
let middle = chart_sides(atlas, chart, a.add(b).scale(0.5));
start.iter().find_map(|&(side, edge, qa)| {
let (_, _, qb) = *end.iter().find(|(candidate, _, _)| *candidate == side)?;
middle
.iter()
.any(|(candidate, _, _)| *candidate == side)
.then_some((edge, qa, qb))
})
}
fn block_edge_span(
points: &[EdgePoint],
start: usize,
end: usize,
blocking: &mut HashSet<(usize, usize)>,
) -> bool {
let position = |vertex: usize| points.iter().position(|point| point.vertex == vertex);
let (Some(first), Some(last)) = (position(start), position(end)) else {
return false;
};
let (lo, hi) = (first.min(last), first.max(last));
for index in lo..hi {
blocking.insert(ordered(points[index].vertex, points[index + 1].vertex));
}
first != last
}
fn chart_uv(atlas: &SphereAtlas, chart: usize, point: Vec3) -> Option<[f64; 2]> {
let (s, t) = atlas.coordinates(chart, point)?;
Some([s.clamp(-1.0, 1.0), t.clamp(-1.0, 1.0)])
}
fn mesh_chart(
face_id: u32,
atlas: &SphereAtlas,
chart: usize,
edge_points: &[Vec<EdgePoint>],
chains: &[ChartChain],
vertices: &mut AtlasVertices,
chord_tolerance: f64,
) -> Result<Option<Vec<[usize; 3]>>, String> {
use spade::{ConstrainedDelaunayTriangulation, Point2, Triangulation};
let refuse = |why: &str, points: &[([f64; 2], usize)], ring: usize| -> Option<Vec<[usize; 3]>> {
if std::env::var("BREP_DEBUG_SPHERE_CHARTS").is_ok() {
eprintln!("[sphere-atlas] face {face_id} chart {chart}: {why}");
for (slot, (uv, vertex)) in points.iter().enumerate() {
if uv[0].abs() < 1.0 - 1e-9 && uv[1].abs() < 1.0 - 1e-9 {
continue;
}
eprintln!(
"[sphere-atlas] {} slot {slot} v{vertex} ({:.15}, {:.15})",
if slot < ring { "ring " } else { "chain" },
uv[0],
uv[1]
);
}
}
None
};
let mut ring: Vec<([f64; 2], usize)> = Vec::new();
for (side, ascending) in [
(ChartSide::TMinus, true),
(ChartSide::SPlus, true),
(ChartSide::TPlus, false),
(ChartSide::SMinus, false),
] {
let (edge, sign) = side.edge(chart);
let list = &edge_points[edge];
let mut side_points: Vec<([f64; 2], usize)> = list
.iter()
.map(|point| {
let free = sign * point.q;
let (s, t) = side.coords(free);
([s, t], point.vertex)
})
.collect();
side_points.sort_by(|a, b| {
let axis = usize::from(matches!(side, ChartSide::SPlus | ChartSide::SMinus));
a.0[axis].total_cmp(&b.0[axis])
});
if !ascending {
side_points.reverse();
}
side_points.pop();
ring.extend(side_points);
}
if ring.len() < 4 {
return Ok(refuse("ring has fewer than four points", &[], 0));
}
let mut points: Vec<([f64; 2], usize)> = ring.clone();
let mut constraints: Vec<(usize, usize)> = Vec::new();
for index in 0..ring.len() {
constraints.push((index, (index + 1) % ring.len()));
}
let mut seen: HashMap<usize, usize> = ring
.iter()
.enumerate()
.map(|(slot, (_, vertex))| (*vertex, slot))
.collect();
for chain in chains {
let mut previous: Option<usize> = None;
for (uv, vertex) in &chain.points {
let slot = *seen.entry(*vertex).or_insert_with(|| {
points.push((*uv, *vertex));
points.len() - 1
});
if let Some(previous) = previous {
if previous != slot {
constraints.push((previous, slot));
}
}
previous = Some(slot);
}
}
let divisions = chart_grid_divisions(atlas.radius, chord_tolerance);
let grid = chart_grid_parameters(divisions);
let clearance = CONSTRAINT_CLEARANCE * 2.0 / divisions as f64;
let segments: Vec<([f64; 2], [f64; 2])> = constraints
.iter()
.map(|&(a, b)| (points[a].0, points[b].0))
.collect();
let mut interior: Vec<[f64; 2]> = Vec::new();
for &s in &grid {
for &t in &grid {
let uv = [s, t];
if segments
.iter()
.any(|(a, b)| distance_to_segment(uv, *a, *b) < clearance)
{
continue;
}
interior.push(uv);
}
}
let mut triangulation = ConstrainedDelaunayTriangulation::<AtlasPoint>::new();
let mut handles = Vec::with_capacity(points.len());
for (slot, (uv, _)) in points.iter().enumerate() {
let handle = triangulation
.insert(AtlasPoint {
position: Point2::new(uv[0], uv[1]),
authored: slot,
})
.map_err(|error| format!("sphere atlas: chart insert failed: {error:?}"))?;
if handle.index() != slot {
return Ok(refuse(
&format!(
"point {slot} at ({:.15}, {:.15}) coincided with an existing vertex",
points[slot].0[0], points[slot].0[1]
),
&points,
ring.len(),
));
}
handles.push(handle);
}
for &(a, b) in &constraints {
let added = triangulation.try_add_constraint(handles[a], handles[b]);
if added.len() > 1 || !triangulation.exists_constraint(handles[a], handles[b]) {
return Ok(refuse(
&format!(
"constraint slot {a}-{b} ({:.15},{:.15})-({:.15},{:.15}) became {} edges",
points[a].0[0], points[a].0[1], points[b].0[0], points[b].0[1],
added.len()
),
&points,
ring.len(),
));
}
}
for uv in interior {
insert_free_point(&mut triangulation, atlas, chart, uv, &mut points, vertices)?;
}
for _ in 0..MAX_REFINE_PASSES {
let mut additions: Vec<[f64; 2]> = Vec::new();
for face in triangulation.inner_faces() {
let slots = face.vertices().map(|vertex| vertex.data().authored);
let uv = slots.map(|slot| points[slot].0);
let corners: [Vec3; 3] = [
vertices.positions[points[slots[0]].1],
vertices.positions[points[slots[1]].1],
vertices.positions[points[slots[2]].1],
];
if triangle_sag(atlas, corners) <= chord_tolerance {
continue;
}
let centre = [
(uv[0][0] + uv[1][0] + uv[2][0]) / 3.0,
(uv[0][1] + uv[1][1] + uv[2][1]) / 3.0,
];
if segments
.iter()
.any(|(a, b)| distance_to_segment(centre, *a, *b) < 1e-9)
{
continue;
}
additions.push(centre);
}
if additions.is_empty() {
break;
}
for uv in additions {
insert_free_point(&mut triangulation, atlas, chart, uv, &mut points, vertices)?;
}
}
let mut triangles = Vec::new();
for face in triangulation.inner_faces() {
let slots = face.vertices().map(|vertex| vertex.data().authored);
let uv = slots.map(|slot| points[slot].0);
let mut corners = [
points[slots[0]].1,
points[slots[1]].1,
points[slots[2]].1,
];
if corners[0] == corners[1] || corners[1] == corners[2] || corners[2] == corners[0] {
continue;
}
let area = (uv[1][0] - uv[0][0]) * (uv[2][1] - uv[0][1])
- (uv[2][0] - uv[0][0]) * (uv[1][1] - uv[0][1]);
if area < 0.0 {
corners.swap(1, 2);
}
triangles.push(corners);
}
Ok(Some(triangles))
}
fn insert_free_point(
triangulation: &mut spade::ConstrainedDelaunayTriangulation<AtlasPoint>,
atlas: &SphereAtlas,
chart: usize,
uv: [f64; 2],
points: &mut Vec<([f64; 2], usize)>,
vertices: &mut AtlasVertices,
) -> Result<(), String> {
use spade::{Point2, Triangulation};
let before = triangulation.num_vertices();
let slot = points.len();
let inserted = triangulation.insert(AtlasPoint {
position: Point2::new(uv[0], uv[1]),
authored: slot,
});
if inserted.is_err() || triangulation.num_vertices() != before + 1 {
return Ok(());
}
points.push((uv, vertices.push(atlas.point(chart, uv[0], uv[1])?)));
Ok(())
}
#[derive(Clone, Copy)]
struct AtlasPoint {
position: spade::Point2<f64>,
authored: usize,
}
impl spade::HasPosition for AtlasPoint {
type Scalar = f64;
fn position(&self) -> spade::Point2<f64> {
self.position
}
}
fn triangle_sag(atlas: &SphereAtlas, corners: [Vec3; 3]) -> f64 {
let normal = corners[1]
.sub(corners[0])
.cross(corners[2].sub(corners[0]));
let Ok(unit) = normal.normalized() else {
return 0.0;
};
let distance = corners[0].sub(atlas.centre).dot(unit).abs();
(atlas.radius - distance).max(0.0)
}
fn distance_to_segment(point: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let length = dx * dx + dy * dy;
if length <= 0.0 {
return ((point[0] - a[0]).powi(2) + (point[1] - a[1]).powi(2)).sqrt();
}
let t = (((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / length).clamp(0.0, 1.0);
((point[0] - a[0] - t * dx).powi(2) + (point[1] - a[1] - t * dy).powi(2)).sqrt()
}
fn material_regions(
triangles: &[[usize; 3]],
blocking: &HashSet<(usize, usize)>,
vertices: &AtlasVertices,
atlas: &SphereAtlas,
region: &SphericalRegion,
) -> Vec<bool> {
if region.is_whole_sphere() {
return vec![true; triangles.len()];
}
let mut adjacency: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
for (index, triangle) in triangles.iter().enumerate() {
for (a, b) in [(0, 1), (1, 2), (2, 0)] {
let pair = ordered(triangle[a], triangle[b]);
if blocking.contains(&pair) {
continue;
}
adjacency.entry(pair).or_default().push(index);
}
}
let centroid = |triangle: &[usize; 3]| -> Vec3 {
vertices.positions[triangle[0]]
.add(vertices.positions[triangle[1]])
.add(vertices.positions[triangle[2]])
.scale(1.0 / 3.0)
};
let mut label = vec![usize::MAX; triangles.len()];
let mut regions: Vec<Vec<usize>> = Vec::new();
for seed in 0..triangles.len() {
if label[seed] != usize::MAX {
continue;
}
let id = regions.len();
let mut members = Vec::new();
let mut queue = vec![seed];
label[seed] = id;
while let Some(current) = queue.pop() {
members.push(current);
for (a, b) in [(0, 1), (1, 2), (2, 0)] {
let pair = ordered(triangles[current][a], triangles[current][b]);
let Some(neighbours) = adjacency.get(&pair) else {
continue;
};
for &neighbour in neighbours {
if label[neighbour] == usize::MAX {
label[neighbour] = id;
queue.push(neighbour);
}
}
}
}
regions.push(members);
}
if std::env::var("BREP_DEBUG_SPHERE_CHARTS").is_ok() {
let mut present: HashSet<(usize, usize)> = HashSet::new();
for triangle in triangles {
for (a, b) in [(0, 1), (1, 2), (2, 0)] {
present.insert(ordered(triangle[a], triangle[b]));
}
}
let gaps: Vec<&(usize, usize)> =
blocking.iter().filter(|pair| !present.contains(pair)).collect();
eprintln!(
"[sphere-atlas] regions={} sizes={:?} blocking={} gaps={}",
regions.len(),
regions.iter().map(Vec::len).take(8).collect::<Vec<_>>(),
blocking.len(),
gaps.len()
);
for pair in gaps.iter().take(6) {
eprintln!(
"[sphere-atlas] gap {:?} -> {:?}",
vertices.positions[pair.0], vertices.positions[pair.1]
);
}
}
let debug = std::env::var("BREP_DEBUG_SPHERE_CHARTS").is_ok();
let mut keep = vec![false; triangles.len()];
for members in ®ions {
let stride = (members.len() / 8).max(1);
let probes: Vec<(usize, bool)> = members
.iter()
.step_by(stride)
.take(16)
.map(|&index| {
let point = centroid(&triangles[index]);
(
region.separation(atlas.centre, point),
region.contains(atlas.centre, point),
)
})
.collect();
let sample: Vec<bool> = probes.iter().map(|(_, inside)| *inside).collect();
if debug {
eprintln!(
"[sphere-atlas] region size={} best separation={:?} probes={:?}",
members.len(),
probes.first().map(|(separation, _)| *separation),
sample
);
}
let unanimous = sample.first().map(|first| sample.iter().all(|value| value == first));
match unanimous {
Some(true) => {
let inside = sample[0];
for &index in members {
keep[index] = inside;
}
}
_ => {
for &index in members {
keep[index] = region.contains(atlas.centre, centroid(&triangles[index]));
}
}
}
}
keep
}