#![allow(dead_code)]
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CsgOp {
Union,
Intersect,
Subtract,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct SdfSphere {
pub center: [f32; 3],
pub radius: f32,
}
impl SdfSphere {
#[allow(dead_code)]
pub fn eval(&self, p: [f32; 3]) -> f32 {
let dx = p[0] - self.center[0];
let dy = p[1] - self.center[1];
let dz = p[2] - self.center[2];
(dx * dx + dy * dy + dz * dz).sqrt() - self.radius
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct SdfBox {
pub center: [f32; 3],
pub half_size: [f32; 3],
}
impl SdfBox {
#[allow(dead_code)]
pub fn eval(&self, p: [f32; 3]) -> f32 {
let qx = (p[0] - self.center[0]).abs() - self.half_size[0];
let qy = (p[1] - self.center[1]).abs() - self.half_size[1];
let qz = (p[2] - self.center[2]).abs() - self.half_size[2];
let mx = qx.max(0.0);
let my = qy.max(0.0);
let mz = qz.max(0.0);
(mx * mx + my * my + mz * mz).sqrt() + qx.max(qy).max(qz).min(0.0)
}
}
#[allow(dead_code)]
pub fn csg_combine(a: f32, b: f32, op: CsgOp) -> f32 {
match op {
CsgOp::Union => a.min(b),
CsgOp::Intersect => a.max(b),
CsgOp::Subtract => a.max(-b),
}
}
#[allow(dead_code)]
pub fn sample_csg_grid(
a: &SdfSphere,
b: &SdfBox,
op: CsgOp,
resolution: usize,
extent: f32,
) -> Vec<[f32; 3]> {
let mut inside = Vec::new();
let step = 2.0 * extent / resolution as f32;
for ix in 0..resolution {
for iy in 0..resolution {
for iz in 0..resolution {
let x = -extent + (ix as f32 + 0.5) * step;
let y = -extent + (iy as f32 + 0.5) * step;
let z = -extent + (iz as f32 + 0.5) * step;
let p = [x, y, z];
let val = csg_combine(a.eval(p), b.eval(p), op);
if val < 0.0 {
inside.push(p);
}
}
}
}
inside
}
#[allow(dead_code)]
pub fn csg_inside_count(pts: &[[f32; 3]]) -> usize {
pts.len()
}
#[allow(dead_code)]
pub fn csg_to_mesh(
a: &SdfSphere,
b: &SdfBox,
op: CsgOp,
resolution: usize,
extent: f32,
) -> crate::mesh::MeshBuffers {
use crate::marching_cubes::{marching_cubes, ScalarField};
let res = resolution.max(2);
let step = 2.0 * extent / res as f32;
let pts = res + 1;
let mut field = ScalarField::new(
[pts, pts, pts],
[-extent, -extent, -extent],
[step, step, step],
);
for iz in 0..pts {
for iy in 0..pts {
for ix in 0..pts {
let p = field.world_pos(ix, iy, iz);
let val = csg_combine(a.eval(p), b.eval(p), op);
field.set(ix, iy, iz, val);
}
}
}
marching_cubes(&field, 0.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn unit_sphere() -> SdfSphere {
SdfSphere {
center: [0.0, 0.0, 0.0],
radius: 1.0,
}
}
fn unit_box() -> SdfBox {
SdfBox {
center: [0.0, 0.0, 0.0],
half_size: [1.0, 1.0, 1.0],
}
}
#[test]
fn sphere_inside() {
let s = unit_sphere();
assert!(s.eval([0.0, 0.0, 0.0]) < 0.0);
}
#[test]
fn sphere_outside() {
let s = unit_sphere();
assert!(s.eval([2.0, 0.0, 0.0]) > 0.0);
}
#[test]
fn box_inside() {
let b = unit_box();
assert!(b.eval([0.0, 0.0, 0.0]) < 0.0);
}
#[test]
fn box_outside() {
let b = unit_box();
assert!(b.eval([2.0, 0.0, 0.0]) > 0.0);
}
#[test]
fn union_superset() {
let s = unit_sphere();
let b = unit_box();
let u = sample_csg_grid(&s, &b, CsgOp::Union, 4, 2.0);
let i = sample_csg_grid(&s, &b, CsgOp::Intersect, 4, 2.0);
assert!(u.len() >= i.len());
}
#[test]
fn subtract_less_than_a() {
let s = unit_sphere();
let b = unit_box();
let sub = sample_csg_grid(&s, &b, CsgOp::Subtract, 4, 2.0);
let union = sample_csg_grid(&s, &b, CsgOp::Union, 4, 2.0);
assert!(sub.len() < union.len());
}
#[test]
fn csg_combine_union() {
assert!((csg_combine(-1.0, 2.0, CsgOp::Union) - (-1.0)).abs() < 1e-6);
}
#[test]
fn csg_combine_intersect() {
assert!((csg_combine(-1.0, 2.0, CsgOp::Intersect) - 2.0).abs() < 1e-6);
}
#[test]
fn csg_combine_subtract() {
assert!((csg_combine(-1.0, -2.0, CsgOp::Subtract) - 2.0).abs() < 1e-6);
}
#[test]
fn inside_count_matches() {
let s = unit_sphere();
let b = unit_box();
let pts = sample_csg_grid(&s, &b, CsgOp::Union, 4, 2.0);
assert_eq!(csg_inside_count(&pts), pts.len());
}
#[test]
fn csg_to_mesh_sphere_union_box_non_empty() {
let s = unit_sphere();
let b = unit_box();
let mesh = csg_to_mesh(&s, &b, CsgOp::Union, 16, 2.0);
assert!(
!mesh.positions.is_empty(),
"union of sphere and box should produce a non-empty mesh, got {} vertices",
mesh.positions.len()
);
}
}