use alloc::vec::Vec;
use super::{BoundedSdf, COMPACT_DOMAIN, FieldBound, ReferenceField, cube_domain};
use crate::vec3::{dot, length, scale, sub};
use crate::{Real, Sdf};
const BALL_RADIUS: f64 = 1.55;
const TUBE: f64 = 0.28;
const RHO: f64 = 0.75;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DrilledBall<R: Real, const G: usize> {
pub radius: R,
pub bore: R,
pub bores: [[R; 2]; G],
}
impl<R: Real, const G: usize> DrilledBall<R, G> {
#[inline]
fn nearest_bore(&self, p: [R; 3]) -> (R, [R; 2]) {
let mut best = (R::INFINITY, [R::ZERO; 2]);
for c in &self.bores {
let d = [p[0] - c[0], p[1] - c[1]];
let rho = (d[0] * d[0] + d[1] * d[1]).sqrt();
let signed = rho - self.bore;
if signed < best.0 {
best = (signed, d);
}
}
best
}
}
impl<R: Real, const G: usize> Sdf for DrilledBall<R, G> {
type Scalar = R;
#[inline]
fn sample(&self, p: [R; 3]) -> R {
let ball = length(p) - self.radius;
let (drilled, _) = self.nearest_bore(p);
ball.max(-drilled)
}
#[inline]
fn gradient(&self, p: [R; 3]) -> [R; 3] {
let norm = length(p);
let ball = norm - self.radius;
let (drilled, d) = self.nearest_bore(p);
if ball >= -drilled {
if norm > R::ZERO {
scale(p, R::ONE / norm)
} else {
[R::ZERO; 3]
}
} else {
let rho = drilled + self.bore;
if rho > R::ZERO {
[-d[0] / rho, -d[1] / rho, R::ZERO]
} else {
[R::ZERO; 3]
}
}
}
}
impl<R: Real, const G: usize> ReferenceField for DrilledBall<R, G> {
const NAME: &'static str = match G {
1 => "ball_drilled_g1",
2 => "ball_drilled_g2",
3 => "ball_drilled_g3",
_ => "ball_drilled",
};
fn domain(&self) -> ([R; 3], [R; 3]) {
cube_domain(COMPACT_DOMAIN)
}
fn closed_in_domain(&self) -> bool {
true
}
fn expected_euler(&self) -> Option<i64> {
#[allow(clippy::cast_possible_wrap)]
Some(2 - 2 * G as i64)
}
fn bound(&self) -> FieldBound {
FieldBound::Lipschitz { l: 1.0 }
}
}
impl<R: Real, const G: usize> BoundedSdf for DrilledBall<R, G> {
fn value_bound(&self) -> FieldBound {
FieldBound::Lipschitz { l: 1.0 }
}
}
fn ring<R: Real, const G: usize>(rho: f64) -> [[R; 2]; G] {
core::array::from_fn(|k| {
#[allow(clippy::cast_precision_loss)]
let a = core::f64::consts::TAU * (k as f64) / (G as f64);
[
R::from_f64(rho * libm::cos(a)),
R::from_f64(rho * libm::sin(a)),
]
})
}
fn drilled<R: Real, const G: usize>() -> DrilledBall<R, G> {
DrilledBall {
radius: R::from_f64(BALL_RADIUS),
bore: R::from_f64(TUBE),
bores: ring(RHO),
}
}
#[must_use]
pub fn ball_drilled_g1<R: Real>() -> DrilledBall<R, 1> {
DrilledBall {
radius: R::from_f64(BALL_RADIUS),
bore: R::from_f64(TUBE),
bores: [[R::ZERO, R::ZERO]],
}
}
#[must_use]
pub fn ball_drilled_g2<R: Real>() -> DrilledBall<R, 2> {
drilled()
}
#[must_use]
pub fn ball_drilled_g3<R: Real>() -> DrilledBall<R, 3> {
drilled()
}
#[inline]
fn point_segment<R: Real>(p: [R; 3], a: [R; 3], b: [R; 3]) -> (R, [R; 3]) {
let ab = sub(b, a);
let ap = sub(p, a);
let len2 = dot(ab, ab);
let t = if len2 > R::ZERO {
(dot(ap, ab) / len2).clamp(R::ZERO, R::ONE)
} else {
R::ZERO
};
let offset = sub(ap, scale(ab, t));
(length(offset), offset)
}
#[derive(Clone, Debug, PartialEq)]
pub struct ThickenedGraph<R: Real, const G: usize> {
pub nodes: Vec<[R; 3]>,
pub edges: Vec<[usize; 2]>,
pub tube: R,
}
impl<R: Real, const G: usize> ThickenedGraph<R, G> {
#[inline]
fn nearest_edge(&self, p: [R; 3]) -> (R, [R; 3]) {
let mut best = (R::INFINITY, [R::ZERO; 3]);
for e in &self.edges {
if let (Some(a), Some(b)) = (self.nodes.get(e[0]), self.nodes.get(e[1])) {
let candidate = point_segment(p, *a, *b);
if candidate.0 < best.0 {
best = candidate;
}
}
}
best
}
}
impl<R: Real, const G: usize> Sdf for ThickenedGraph<R, G> {
type Scalar = R;
#[inline]
fn sample(&self, p: [R; 3]) -> R {
self.nearest_edge(p).0 - self.tube
}
#[inline]
fn gradient(&self, p: [R; 3]) -> [R; 3] {
let (d, offset) = self.nearest_edge(p);
if d > R::ZERO && d < R::INFINITY {
scale(offset, R::ONE / d)
} else {
[R::ZERO; 3]
}
}
}
impl<R: Real, const G: usize> ReferenceField for ThickenedGraph<R, G> {
const NAME: &'static str = match G {
2 => "graph_theta_g2",
3 => "graph_k4_g3",
5 => "graph_cube_g5",
_ => "thickened_graph",
};
fn domain(&self) -> ([R; 3], [R; 3]) {
cube_domain(COMPACT_DOMAIN)
}
fn closed_in_domain(&self) -> bool {
true
}
fn expected_euler(&self) -> Option<i64> {
#[allow(clippy::cast_possible_wrap)]
Some(2 - 2 * G as i64)
}
fn bound(&self) -> FieldBound {
FieldBound::Lipschitz { l: 1.0 }
}
}
impl<R: Real, const G: usize> BoundedSdf for ThickenedGraph<R, G> {
fn value_bound(&self) -> FieldBound {
FieldBound::Lipschitz { l: 1.0 }
}
}
fn graphed<R: Real, const G: usize>(
nodes: Vec<[R; 3]>,
edges: Vec<[usize; 2]>,
) -> ThickenedGraph<R, G> {
debug_assert_eq!(
edges.len() + 1,
nodes.len() + G,
"b1 = E - V + 1 must be the genus"
);
ThickenedGraph {
nodes,
edges,
tube: R::from_f64(TUBE),
}
}
#[must_use]
pub fn graph_theta_g2<R: Real>() -> ThickenedGraph<R, 2> {
let pole = R::ONE;
let mut nodes = alloc::vec![[R::ZERO, R::ZERO, pole], [R::ZERO, R::ZERO, -pole]];
for w in ring::<R, 3>(1.0) {
nodes.push([w[0], w[1], R::ZERO]);
}
graphed(
nodes,
alloc::vec![[0, 2], [0, 3], [0, 4], [1, 2], [1, 3], [1, 4]],
)
}
#[must_use]
pub fn graph_k4_g3<R: Real>() -> ThickenedGraph<R, 3> {
let a = R::from_f64(0.72);
graphed(
alloc::vec![[a, a, a], [a, -a, -a], [-a, a, -a], [-a, -a, a]],
alloc::vec![[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]],
)
}
#[must_use]
pub fn graph_cube_g5<R: Real>() -> ThickenedGraph<R, 5> {
let a = R::from_f64(RHO);
let coord = |bit: u32, i: u32| if i & bit == 0 { -a } else { a };
let nodes: Vec<[R; 3]> = (0..8u32)
.map(|i| [coord(1, i), coord(2, i), coord(4, i)])
.collect();
let mut edges: Vec<[usize; 2]> = Vec::with_capacity(12);
for i in 0..8u32 {
for bit in [1u32, 2, 4] {
let j = i ^ bit;
if j > i {
edges.push([i as usize, j as usize]);
}
}
}
graphed(nodes, edges)
}