use crate::compiled::{eval_compiled, CompiledSdf};
use crate::eval::{eval, eval_gradient};
use crate::types::SdfNode;
use glam::Vec3;
use rayon::prelude::*;
use std::sync::Arc;
#[derive(Debug, Clone, Copy)]
pub struct ShellConfig {
pub inner_offset: f32,
pub outer_offset: f32,
}
impl ShellConfig {
#[inline]
pub const fn new(inner_offset: f32, outer_offset: f32) -> Self {
Self {
inner_offset,
outer_offset,
}
}
#[inline]
pub const fn uniform(thickness: f32) -> Self {
Self {
inner_offset: thickness,
outer_offset: thickness,
}
}
#[inline]
pub fn wall_thickness(self) -> f32 {
self.inner_offset + self.outer_offset
}
}
impl Default for ShellConfig {
fn default() -> Self {
Self {
inner_offset: 0.1,
outer_offset: 0.1,
}
}
}
#[inline]
pub fn eval_shell(node: &SdfNode, point: Vec3, config: &ShellConfig) -> f32 {
let d = eval(node, point);
let outer_surface = d - config.outer_offset;
let inner_surface = -(d + config.inner_offset);
outer_surface.max(inner_surface)
}
pub fn eval_shell_batch(node: &SdfNode, points: &[Vec3], config: &ShellConfig) -> Vec<f32> {
points
.iter()
.map(|&p| eval_shell(node, p, config))
.collect()
}
pub fn eval_shell_batch_parallel(
node: &SdfNode,
points: &[Vec3],
config: &ShellConfig,
) -> Vec<f32> {
points
.par_iter()
.map(|&p| eval_shell(node, p, config))
.collect()
}
pub fn eval_shell_gradient(node: &SdfNode, point: Vec3, config: &ShellConfig) -> Vec3 {
let d = eval(node, point);
let g = eval_gradient(node, point);
let outer_surface = d - config.outer_offset;
let inner_surface = -(d + config.inner_offset);
if outer_surface >= inner_surface {
g
} else {
-g
}
}
#[inline]
pub fn eval_shell_compiled(compiled: &CompiledSdf, point: Vec3, config: &ShellConfig) -> f32 {
let d = eval_compiled(compiled, point);
let outer_surface = d - config.outer_offset;
let inner_surface = -(d + config.inner_offset);
outer_surface.max(inner_surface)
}
pub fn eval_shell_compiled_batch_parallel(
compiled: &CompiledSdf,
points: &[Vec3],
config: &ShellConfig,
) -> Vec<f32> {
points
.par_iter()
.map(|&p| eval_shell_compiled(compiled, p, config))
.collect()
}
pub fn shell_node(child: Arc<SdfNode>, config: ShellConfig) -> SdfNode {
let half_thick = f32::midpoint(config.inner_offset, config.outer_offset);
let center_offset = (config.outer_offset - config.inner_offset) * 0.5;
if center_offset.abs() < 1e-6 {
SdfNode::Onion {
child,
thickness: half_thick,
}
} else {
SdfNode::Onion {
child: Arc::new(SdfNode::Round {
child,
radius: center_offset,
}),
thickness: half_thick,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_outside() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.1);
let d = eval_shell(&sphere, Vec3::new(2.0, 0.0, 0.0), &config);
assert!(d > 0.0);
}
#[test]
fn shell_inside_wall() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.1);
let d = eval_shell(&sphere, Vec3::new(1.0, 0.0, 0.0), &config);
assert!(d < 0.0);
}
#[test]
fn shell_hollow_center() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.1);
let d = eval_shell(&sphere, Vec3::ZERO, &config);
assert!(d > 0.0, "Center should be outside shell (hollow)");
}
#[test]
fn shell_outer_boundary() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.2);
let d = eval_shell(&sphere, Vec3::new(1.2, 0.0, 0.0), &config);
assert!(d.abs() < 1e-4, "Should be on outer surface, got {}", d);
}
#[test]
fn shell_inner_boundary() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.2);
let d = eval_shell(&sphere, Vec3::new(0.9, 0.0, 0.0), &config);
assert!(d.abs() < 1e-4, "Should be on inner surface, got {}", d);
}
#[test]
fn shell_asymmetric() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.05, 0.3);
assert!((config.wall_thickness() - 0.35).abs() < 1e-6);
let d = eval_shell(&sphere, Vec3::new(1.0, 0.0, 0.0), &config);
assert!(d < 0.0);
}
#[test]
fn shell_zero_thickness() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.0, 0.0);
let d = eval_shell(&sphere, Vec3::new(1.0, 0.0, 0.0), &config);
assert!(d.abs() < 1e-5);
}
#[test]
fn shell_batch() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.1);
let points = vec![
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::ZERO,
];
let results = eval_shell_batch(&sphere, &points, &config);
assert_eq!(results.len(), 3);
assert!(results[0] > 0.0); assert!(results[1] < 0.0); assert!(results[2] > 0.0); }
#[test]
fn shell_uniform_config() {
let config = ShellConfig::uniform(0.5);
assert_eq!(config.inner_offset, 0.5);
assert_eq!(config.outer_offset, 0.5);
assert!((config.wall_thickness() - 1.0).abs() < 1e-6);
}
#[test]
fn shell_node_symmetric() {
let child = Arc::new(SdfNode::sphere(1.0));
let config = ShellConfig::uniform(0.1);
let node = shell_node(child, config);
match node {
SdfNode::Onion { thickness, .. } => {
assert!((thickness - 0.1).abs() < 1e-6);
}
_ => panic!("Expected Onion node for symmetric shell"),
}
}
#[test]
fn shell_default_config() {
let config = ShellConfig::default();
assert_eq!(config.inner_offset, 0.1);
assert_eq!(config.outer_offset, 0.1);
}
#[test]
fn shell_batch_parallel_matches_sequential() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.1);
let points: Vec<Vec3> = (0..100)
.map(|i| {
let t = i as f32 / 50.0;
Vec3::new(t, 0.0, 0.0)
})
.collect();
let seq = eval_shell_batch(&sphere, &points, &config);
let par = super::eval_shell_batch_parallel(&sphere, &points, &config);
for (s, p) in seq.iter().zip(par.iter()) {
assert!((s - p).abs() < 1e-6);
}
}
#[test]
fn shell_gradient_outer() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.1);
let g = super::eval_shell_gradient(&sphere, Vec3::new(1.05, 0.0, 0.0), &config);
assert!(g.x > 0.5, "gradient x={}", g.x);
}
#[test]
fn shell_gradient_inner() {
let sphere = SdfNode::sphere(1.0);
let config = ShellConfig::new(0.1, 0.1);
let g = super::eval_shell_gradient(&sphere, Vec3::new(0.95, 0.0, 0.0), &config);
assert!(g.x < -0.5, "gradient x={}", g.x);
}
#[test]
fn shell_compiled_matches_interpreted() {
use crate::compiled::CompiledSdf;
let sphere = SdfNode::sphere(1.0);
let compiled = CompiledSdf::compile(&sphere);
let config = ShellConfig::new(0.1, 0.2);
let points = [
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::ZERO,
Vec3::new(0.9, 0.0, 0.0),
Vec3::new(1.2, 0.0, 0.0),
];
for p in points {
let interp = eval_shell(&sphere, p, &config);
let comp = super::eval_shell_compiled(&compiled, p, &config);
assert!(
(interp - comp).abs() < 1e-4,
"mismatch at {:?}: {} vs {}",
p,
interp,
comp
);
}
}
#[test]
fn shell_compiled_batch_parallel() {
use crate::compiled::CompiledSdf;
let sphere = SdfNode::sphere(1.0);
let compiled = CompiledSdf::compile(&sphere);
let config = ShellConfig::new(0.1, 0.1);
let points: Vec<Vec3> = (0..50)
.map(|i| Vec3::new(i as f32 * 0.1, 0.0, 0.0))
.collect();
let results = super::eval_shell_compiled_batch_parallel(&compiled, &points, &config);
assert_eq!(results.len(), 50);
for (i, &p) in points.iter().enumerate() {
let expected = super::eval_shell_compiled(&compiled, p, &config);
assert!((results[i] - expected).abs() < 1e-6);
}
}
#[test]
fn shell_node_asymmetric() {
let child = Arc::new(SdfNode::sphere(1.0));
let config = ShellConfig::new(0.05, 0.3);
let node = shell_node(child, config);
match &node {
SdfNode::Onion { child, thickness } => {
assert!((thickness - 0.175).abs() < 1e-6);
assert!(
matches!(**child, SdfNode::Round { radius, .. } if (radius - 0.125).abs() < 1e-6)
);
}
_ => panic!("Expected Onion(Round) node for asymmetric shell"),
}
}
}