use crate::{AnalyticSurface, NurbsSurface, Vec3};
pub const CHART_COUNT: usize = 6;
pub const CUBE_EDGE_COUNT: usize = 12;
pub const CUBE_CORNER_COUNT: usize = 8;
const ENDPOINT_LAMBDA: f64 = 1e-9;
const CHART_EPSILON: f64 = 1e-12;
#[derive(Clone, Copy, Debug)]
pub struct Chart {
pub normal: Vec3,
pub tangent_s: Vec3,
pub tangent_t: Vec3,
pub axis: usize,
pub sign: f64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChartSide {
SPlus,
SMinus,
TPlus,
TMinus,
}
impl ChartSide {
pub const ALL: [ChartSide; 4] = [
ChartSide::SPlus,
ChartSide::SMinus,
ChartSide::TPlus,
ChartSide::TMinus,
];
pub fn edge(self, chart: usize) -> (usize, f64) {
let axis = chart / 2;
let sign = if chart % 2 == 0 { 1.0 } else { -1.0 };
let b = (axis + 1) % 3;
let c = (axis + 2) % 3;
match self {
ChartSide::SPlus => (cube_edge_index(c, sign, sign), 1.0),
ChartSide::SMinus => (cube_edge_index(c, sign, -sign), 1.0),
ChartSide::TPlus => (cube_edge_index(b, 1.0, sign), sign),
ChartSide::TMinus => (cube_edge_index(b, -1.0, sign), sign),
}
}
pub fn coords(self, f: f64) -> (f64, f64) {
match self {
ChartSide::SPlus => (1.0, f),
ChartSide::SMinus => (-1.0, f),
ChartSide::TPlus => (f, 1.0),
ChartSide::TMinus => (f, -1.0),
}
}
}
pub fn cube_edge_index(varying: usize, sign_first: f64, sign_second: f64) -> usize {
varying * 4 + usize::from(sign_first > 0.0) * 2 + usize::from(sign_second > 0.0)
}
pub fn cube_edge_parts(index: usize) -> (usize, f64, f64) {
let varying = index / 4;
let rest = index % 4;
(
varying,
if rest & 2 != 0 { 1.0 } else { -1.0 },
if rest & 1 != 0 { 1.0 } else { -1.0 },
)
}
pub fn corner_index(signs: [f64; 3]) -> usize {
usize::from(signs[0] > 0.0) | (usize::from(signs[1] > 0.0) << 1) | (usize::from(signs[2] > 0.0) << 2)
}
pub fn cube_edge_corner(edge: usize, positive_end: bool) -> usize {
let (varying, first, second) = cube_edge_parts(edge);
let mut signs = [0.0; 3];
signs[varying] = if positive_end { 1.0 } else { -1.0 };
signs[(varying + 1) % 3] = first;
signs[(varying + 2) % 3] = second;
corner_index(signs)
}
pub fn cube_edge_parameters(divisions: usize) -> Vec<f64> {
let divisions = divisions.max(1);
let half = std::f64::consts::SQRT_2.recip().atan();
(0..=divisions)
.map(|index| {
if index == 0 {
-1.0
} else if index == divisions {
1.0
} else {
let angle = -half + 2.0 * half * index as f64 / divisions as f64;
std::f64::consts::SQRT_2 * angle.tan()
}
})
.collect()
}
pub fn chart_grid_parameters(divisions: usize) -> Vec<f64> {
let divisions = divisions.max(2);
let half = std::f64::consts::FRAC_PI_4;
(1..divisions)
.map(|index| (-half + 2.0 * half * index as f64 / divisions as f64).tan())
.collect()
}
fn angular_step(radius: f64, chord_tolerance: f64) -> Option<f64> {
if !(chord_tolerance > 0.0) || !(radius > 0.0) {
return None;
}
let ratio = 1.0 - (chord_tolerance / radius).min(1.0);
let step = 2.0 * ratio.clamp(-1.0, 1.0).acos();
(step > 0.0 && step.is_finite()).then_some(step)
}
pub fn chart_grid_divisions(radius: f64, chord_tolerance: f64) -> usize {
let divisions = match angular_step(radius, chord_tolerance) {
Some(step) => (((std::f64::consts::FRAC_PI_2 * std::f64::consts::SQRT_2) / step).ceil()
as usize)
.clamp(2, 96),
None => 16,
};
divisions + divisions % 2
}
pub fn cube_edge_divisions(radius: f64, chord_tolerance: f64) -> usize {
let arc = 2.0 * std::f64::consts::SQRT_2.recip().atan();
match angular_step(radius, chord_tolerance) {
Some(step) => (((arc * std::f64::consts::SQRT_2) / step).ceil() as usize).clamp(2, 80),
None => 14,
}
}
#[derive(Clone, Copy, Debug)]
pub struct SphereAtlas {
pub centre: Vec3,
pub radius: f64,
pub basis: [Vec3; 3],
}
impl SphereAtlas {
pub fn of_surface(surface: &NurbsSurface) -> Option<Self> {
Self::of_analytic(surface.analytic()?)
}
pub fn of_analytic(analytic: &AnalyticSurface) -> Option<Self> {
let (centre, radius, basis) = analytic.sphere_frame()?;
(radius > 0.0).then_some(Self {
centre,
radius,
basis,
})
}
pub fn chart(&self, index: usize) -> Chart {
let axis = index / 2;
let sign = if index % 2 == 0 { 1.0 } else { -1.0 };
let b = (axis + 1) % 3;
let c = (axis + 2) % 3;
Chart {
normal: self.basis[axis].scale(sign),
tangent_s: self.basis[b].scale(sign),
tangent_t: self.basis[c],
axis,
sign,
}
}
pub fn direction(&self, chart: usize, s: f64, t: f64) -> Vec3 {
let chart = self.chart(chart);
chart
.normal
.add(chart.tangent_s.scale(s))
.add(chart.tangent_t.scale(t))
}
pub fn point(&self, chart: usize, s: f64, t: f64) -> Result<Vec3, String> {
let direction = self.direction(chart, s, t).normalized()?;
Ok(self.centre.add(direction.scale(self.radius)))
}
pub fn normal_at(&self, chart: usize, s: f64, t: f64) -> Result<Vec3, String> {
self.direction(chart, s, t).normalized()
}
pub fn parameterization_is_outward(&self, surface: &NurbsSurface) -> Result<bool, String> {
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
let (point, du, dv) = surface.deriv1(0.5 * (u0 + u1), 0.5 * (v0 + v1))?;
Ok(du.cross(dv).dot(point.sub(self.centre)) > 0.0)
}
pub fn axis_coordinates(&self, point: Vec3) -> [f64; 3] {
let d = point.sub(self.centre);
[
d.dot(self.basis[0]),
d.dot(self.basis[1]),
d.dot(self.basis[2]),
]
}
pub fn locate(&self, point: Vec3) -> usize {
let x = self.axis_coordinates(point);
let mut best = 0usize;
let mut best_value = f64::NEG_INFINITY;
for chart in 0..CHART_COUNT {
let axis = chart / 2;
let sign = if chart % 2 == 0 { 1.0 } else { -1.0 };
let value = sign * x[axis];
if value > best_value {
best_value = value;
best = chart;
}
}
best
}
pub fn coordinates(&self, chart: usize, point: Vec3) -> Option<(f64, f64)> {
let x = self.axis_coordinates(point);
let chart = self.chart(chart);
let b = (chart.axis + 1) % 3;
let c = (chart.axis + 2) % 3;
let w = chart.sign * x[chart.axis];
if !(w > 0.0) {
return None;
}
Some((chart.sign * x[b] / w, x[c] / w))
}
pub fn contains(&self, chart: usize, point: Vec3) -> bool {
match self.coordinates(chart, point) {
Some((s, t)) => {
let slack = 1.0 + CHART_EPSILON;
s.abs() <= slack && t.abs() <= slack
}
None => false,
}
}
pub fn charts_containing(&self, point: Vec3) -> Vec<usize> {
(0..CHART_COUNT)
.filter(|chart| self.contains(*chart, point))
.collect()
}
pub fn edge_of(&self, point: Vec3, relative_tolerance: f64) -> Option<(usize, f64)> {
self.edges_containing(point, relative_tolerance)
.into_iter()
.next()
}
pub fn edges_containing(&self, point: Vec3, relative_tolerance: f64) -> Vec<(usize, f64)> {
let x = self.axis_coordinates(point);
let scale = x[0].abs().max(x[1].abs()).max(x[2].abs());
if !(scale > 0.0) {
return Vec::new();
}
let tolerance = relative_tolerance.max(CHART_EPSILON) * scale;
let mut found = Vec::new();
for varying in 0..3 {
let b = (varying + 1) % 3;
let c = (varying + 2) % 3;
let pinned = x[b].abs().min(x[c].abs());
if (x[b].abs() - x[c].abs()).abs() <= tolerance
&& pinned >= x[varying].abs() - tolerance
&& pinned > 0.0
{
let magnitude = 0.5 * (x[b].abs() + x[c].abs());
found.push((
cube_edge_index(varying, x[b], x[c]),
(x[varying] / magnitude).clamp(-1.0, 1.0),
));
}
}
found
}
pub fn edge_point(&self, edge: usize, q: f64) -> Result<Vec3, String> {
let (varying, first, second) = cube_edge_parts(edge);
let direction = self.basis[varying]
.scale(q)
.add(self.basis[(varying + 1) % 3].scale(first))
.add(self.basis[(varying + 2) % 3].scale(second))
.normalized()?;
Ok(self.centre.add(direction.scale(self.radius)))
}
pub fn arc_chart_crossings(&self, start: Vec3, end: Vec3) -> Vec<f64> {
let a = self.axis_coordinates(start);
let b = self.axis_coordinates(end);
let scale = (0..3)
.map(|k| a[k].abs().max(b[k].abs()))
.fold(0.0, f64::max);
if !(scale > 0.0) {
return Vec::new();
}
let significant = 1e-9 * scale;
let mut crossings = Vec::new();
for (i, j) in [(0usize, 1usize), (1, 2), (2, 0)] {
for combination in [1.0f64, -1.0] {
let g0 = a[i] - combination * a[j];
let g1 = b[i] - combination * b[j];
if (g0 > 0.0) == (g1 > 0.0) || g0 == g1 {
continue;
}
if g0.abs().max(g1.abs()) <= significant {
continue;
}
let lambda = g0 / (g0 - g1);
if !(lambda > ENDPOINT_LAMBDA && lambda < 1.0 - ENDPOINT_LAMBDA) {
continue;
}
let at = |k: usize| a[k] + (b[k] - a[k]) * lambda;
let tied = at(i).abs().max(at(j).abs());
let third = at(3 - i - j).abs();
if tied >= third - CHART_EPSILON * scale && tied > 0.0 {
crossings.push(lambda);
}
}
}
crossings.sort_by(f64::total_cmp);
crossings.dedup_by(|x, y| (*x - *y).abs() <= 1e-12);
crossings
}
pub fn split_polyline(&self, points: &[Vec3]) -> Result<Vec<ChartSegment>, String> {
let mut segments = Vec::new();
for index in 0..points.len() {
segments.extend(self.split_segment(points[index], points[(index + 1) % points.len()])?);
}
Ok(segments)
}
pub fn split_segment(&self, start: Vec3, end: Vec3) -> Result<Vec<ChartSegment>, String> {
let mut segments = Vec::new();
{
if start.sub(end).length() <= 0.0 {
return Ok(segments);
}
let crossings = self.arc_chart_crossings(start, end);
let da = self.axis_coordinates(start);
let db = self.axis_coordinates(end);
let mut cursor = start;
let mut previous = 0.0f64;
for lambda in crossings.iter().copied().chain(std::iter::once(1.0)) {
let next = if lambda >= 1.0 {
end
} else {
let direction = self.basis[0]
.scale(da[0] + (db[0] - da[0]) * lambda)
.add(self.basis[1].scale(da[1] + (db[1] - da[1]) * lambda))
.add(self.basis[2].scale(da[2] + (db[2] - da[2]) * lambda))
.normalized()?;
self.centre.add(direction.scale(self.radius))
};
let final_piece = lambda >= 1.0;
let distinct = next.sub(cursor).length() > 0.0
&& (final_piece || next.sub(end).length() > 0.0);
if distinct {
let middle = 0.5 * (previous + lambda);
let probe = self.basis[0]
.scale(da[0] + (db[0] - da[0]) * middle)
.add(self.basis[1].scale(da[1] + (db[1] - da[1]) * middle))
.add(self.basis[2].scale(da[2] + (db[2] - da[2]) * middle));
segments.push(ChartSegment {
start: cursor,
end: next,
chart: self.locate(self.centre.add(probe)),
});
}
cursor = next;
previous = lambda;
}
}
Ok(segments)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ChartSegment {
pub start: Vec3,
pub end: Vec3,
pub chart: usize,
}
fn arcs_cross(a0: Vec3, a1: Vec3, na: Vec3, arc: &Arc) -> bool {
let (b0, b1, nb) = (arc.start, arc.end, arc.normal);
if nb.dot(nb) <= 0.0 {
return false;
}
let straddles = |n: Vec3, x0: Vec3, x1: Vec3| {
let (d0, d1) = (n.dot(x0), n.dot(x1));
!((d0 > 0.0 && d1 > 0.0) || (d0 < 0.0 && d1 < 0.0))
};
if !straddles(na, b0, b1) || !straddles(nb, a0, a1) {
return false;
}
let line = na.cross(nb);
if line.length() <= 1e-9 {
return false;
}
let Ok(unit) = line.normalized() else {
return false;
};
let within = |p: Vec3, x0: Vec3, x1: Vec3, n: Vec3| -> bool {
x0.cross(p).dot(n) >= 0.0 && p.cross(x1).dot(n) > 0.0
};
[unit, unit.scale(-1.0)]
.into_iter()
.any(|p| within(p, a0, a1, na) && within(p, b0, b1, nb))
}
#[derive(Clone, Copy, Debug)]
struct Arc {
start: Vec3,
end: Vec3,
normal: Vec3,
}
#[derive(Clone, Debug, Default)]
pub struct SphericalRegion {
arcs: Vec<Arc>,
seed: Option<Vec3>,
}
impl SphericalRegion {
pub fn new(centre: Vec3, polylines: &[Vec<Vec3>], outward_face_normal: bool) -> Self {
let mut segments = Vec::new();
for polyline in polylines {
for index in 0..polyline.len() {
segments.push((polyline[index], polyline[(index + 1) % polyline.len()]));
}
}
Self::from_segments(centre, &segments, outward_face_normal)
}
pub fn from_segments(centre: Vec3, segments: &[(Vec3, Vec3)], outward_face_normal: bool) -> Self {
use std::collections::HashMap;
type Key = [u64; 3];
let key = |p: Vec3| -> Key { [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()] };
let mut counts: HashMap<(Key, Key), usize> = HashMap::new();
let mut directed: Vec<(Vec3, Vec3)> = Vec::new();
for &(a, b) in segments {
if key(a) == key(b) {
continue;
}
*counts.entry((key(a), key(b))).or_insert(0) += 1;
directed.push((a, b));
}
let mut budget: HashMap<(Key, Key), usize> = HashMap::new();
for (&(from, to), &forward) in &counts {
let backward = counts.get(&(to, from)).copied().unwrap_or(0);
budget.insert((from, to), forward.saturating_sub(backward.min(forward)));
}
let unit = |p: Vec3| p.sub(centre).normalized().ok();
let mut arcs: Vec<Arc> = Vec::new();
for (a, b) in directed {
let entry = budget.entry((key(a), key(b))).or_insert(0);
if *entry == 0 {
continue;
}
*entry -= 1;
if let (Some(a), Some(b)) = (unit(a), unit(b)) {
if a.sub(b).length() > 0.0 {
arcs.push(Arc {
start: a,
end: b,
normal: a.cross(b).normalized().unwrap_or_default(),
});
}
}
}
let seed = Self::seed_from(&arcs, outward_face_normal);
Self { arcs, seed }
}
fn seed_from(arcs: &[Arc], outward_face_normal: bool) -> Option<Vec3> {
let longest = arcs.iter().copied().max_by(|x, y| {
x.start
.sub(x.end)
.length()
.total_cmp(&y.start.sub(y.end).length())
})?;
let (a, b) = (longest.start, longest.end);
let middle = a.add(b).normalized().ok()?;
let along = b.sub(a);
let tangent = along.sub(middle.scale(along.dot(middle)));
let normal = if outward_face_normal {
middle
} else {
middle.scale(-1.0)
};
let left = normal.cross(tangent).normalized().ok()?;
let mut step = 0.1 * a.sub(b).length().max(1e-9);
for _ in 0..24 {
let inside = middle.add(left.scale(step)).normalized().ok()?;
let outside = middle.sub(left.scale(step)).normalized().ok()?;
if Self::crossings(arcs, inside, outside) == 1 {
return Some(inside);
}
step *= 0.5;
}
None
}
fn crossings(arcs: &[Arc], from: Vec3, to: Vec3) -> usize {
let Ok(leg) = from.cross(to).normalized() else {
return 0;
};
arcs.iter()
.filter(|arc| arcs_cross(from, to, leg, arc))
.count()
}
fn parity(arcs: &[Arc], from: Vec3, to: Vec3) -> bool {
let mut even = 0usize;
let mut odd = 0usize;
for index in 0..3 {
let Some(via) = Self::via_point(from, to, index) else {
continue;
};
let count = Self::crossings(arcs, from, via) + Self::crossings(arcs, via, to);
if count % 2 == 0 {
even += 1;
} else {
odd += 1;
}
}
even >= odd
}
fn via_point(from: Vec3, to: Vec3, index: usize) -> Option<Vec3> {
let axis = from.cross(to);
let base = match axis.normalized() {
Ok(unit) => unit,
Err(_) => from.perpendicular().ok()?,
};
let other = base.cross(from).normalized().ok()?;
let angle = std::f64::consts::TAU * index as f64 / 3.0;
base.scale(angle.cos())
.add(other.scale(angle.sin()))
.normalized()
.ok()
}
pub fn is_whole_sphere(&self) -> bool {
self.arcs.is_empty()
}
pub fn is_decidable(&self) -> bool {
self.arcs.is_empty() || self.seed.is_some()
}
pub fn contains(&self, centre: Vec3, point: Vec3) -> bool {
let Ok(probe) = point.sub(centre).normalized() else {
return false;
};
let Some(seed) = self.seed else {
return true;
};
Self::parity(&self.arcs, seed, probe)
}
pub fn separation(&self, centre: Vec3, point: Vec3) -> usize {
let Ok(probe) = point.sub(centre).normalized() else {
return usize::MAX;
};
match self.seed {
Some(seed) => Self::via_point(seed, probe, 0)
.map(|via| {
Self::crossings(&self.arcs, seed, via) + Self::crossings(&self.arcs, via, probe)
})
.unwrap_or(usize::MAX),
None => 0,
}
}
}
pub fn canonicalize_points(points: &mut [Vec3], relative_tolerance: f64) {
let scale = points
.iter()
.map(|p| p.x.abs().max(p.y.abs()).max(p.z.abs()))
.fold(0.0, f64::max)
.max(1.0);
let cell = (relative_tolerance * scale).max(f64::MIN_POSITIVE);
let mut table: std::collections::HashMap<[i64; 3], Vec3> = std::collections::HashMap::new();
for point in points.iter_mut() {
let base = [
(point.x / cell).round() as i64,
(point.y / cell).round() as i64,
(point.z / cell).round() as i64,
];
let mut found = None;
'search: for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
let probe = [base[0] + dx, base[1] + dy, base[2] + dz];
if let Some(&existing) = table.get(&probe) {
if existing.sub(*point).length() <= cell {
found = Some(existing);
break 'search;
}
}
}
}
}
match found {
Some(existing) => *point = existing,
None => {
table.insert(base, *point);
}
}
}
}