use glam::Vec3;
use numpy::ndarray::Array3;
use numpy::{IntoPyArray, PyArray3};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
#[cfg(feature = "volume")]
use pyo3::types::PyBytes;
use pyo3::types::{PyList, PyTuple};
use super::node::PySdfNode;
use crate::eval::eval_batch_parallel;
use crate::io::load;
use crate::mesh::{sdf_to_mesh as core_sdf_to_mesh, MarchingCubesConfig};
use crate::types::SdfNode;
#[pyfunction]
pub fn sphere(radius: f32) -> PySdfNode {
PySdfNode {
inner: SdfNode::sphere(radius),
}
}
#[pyfunction]
#[pyo3(name = "box")]
pub fn make_box(sx: f32, sy: f32, sz: f32) -> PySdfNode {
PySdfNode {
inner: SdfNode::box3d(sx, sy, sz),
}
}
#[pyfunction]
pub fn torus(major_radius: f32, minor_radius: f32) -> PySdfNode {
PySdfNode {
inner: SdfNode::torus(major_radius, minor_radius),
}
}
#[pyfunction]
#[pyo3(signature = (node, bounds = (-2.0, 2.0), resolution = 48))]
pub fn sdf_to_mesh<'py>(
py: Python<'py>,
node: &PySdfNode,
bounds: (f32, f32),
resolution: usize,
) -> PyResult<(Bound<'py, PyList>, Bound<'py, PyList>)> {
if resolution == 0 {
return Err(PyValueError::new_err("resolution must be > 0"));
}
if bounds.1 <= bounds.0 {
return Err(PyValueError::new_err(
"bounds.1 (max) must be > bounds.0 (min)",
));
}
let config = MarchingCubesConfig {
resolution,
iso_level: 0.0,
compute_normals: false,
..Default::default()
};
let min = Vec3::splat(bounds.0);
let max = Vec3::splat(bounds.1);
let node_ref = &node.inner;
let mesh = py.detach(|| core_sdf_to_mesh(node_ref, min, max, &config));
let verts = PyList::empty(py);
for v in &mesh.vertices {
let p = v.position;
let t = PyTuple::new(py, [p.x, p.y, p.z].iter().copied())?;
verts.append(t)?;
}
let faces = PyList::empty(py);
for tri in mesh.indices.chunks_exact(3) {
let f = PyList::new(py, tri.iter().copied().map(|i| i as usize))?;
faces.append(f)?;
}
Ok((verts, faces))
}
#[pyfunction]
pub fn load_asdf(py: Python<'_>, path: &str) -> PyResult<PySdfNode> {
let path = path.to_string();
let tree = py
.detach(|| load(&path))
.map_err(|e| PyValueError::new_err(format!("Load error: {}", e)))?;
Ok(PySdfNode { inner: tree.root })
}
#[cfg(feature = "volume")]
#[pyfunction]
#[pyo3(signature = (node, bounds = (-2.0, 2.0), resolution = 64))]
pub fn bake_to_vdb<'py>(
py: Python<'py>,
node: &PySdfNode,
bounds: (f32, f32),
resolution: u32,
) -> PyResult<Bound<'py, PyBytes>> {
use crate::volume::{bake_volume as cpu_bake, BakeConfig};
if resolution == 0 {
return Err(PyValueError::new_err("resolution must be > 0"));
}
if bounds.1 <= bounds.0 {
return Err(PyValueError::new_err(
"bounds.1 (max) must be > bounds.0 (min)",
));
}
let min = Vec3::splat(bounds.0);
let max = Vec3::splat(bounds.1);
let cfg = BakeConfig {
resolution: [resolution, resolution, resolution],
bounds_min: min,
bounds_max: max,
generate_mips: false,
..Default::default()
};
let node_ref = &node.inner;
let volume = py.detach(|| cpu_bake(node_ref, &cfg));
let payload_bytes = volume.data.len() * 4;
let mut buf: Vec<u8> = Vec::with_capacity(40 + payload_bytes);
buf.extend_from_slice(b"ALICEVDB1");
buf.extend_from_slice(&[0u8; 3]);
buf.extend_from_slice(&resolution.to_le_bytes());
for c in [min.x, min.y, min.z, max.x, max.y, max.z] {
buf.extend_from_slice(&c.to_le_bytes());
}
for v in &volume.data {
buf.extend_from_slice(&v.to_le_bytes());
}
Ok(PyBytes::new(py, &buf))
}
#[pyfunction]
#[pyo3(signature = (node, z, width = 512, height = 512, bounds = (-2.0, 2.0)))]
pub fn render_slice_2d<'py>(
py: Python<'py>,
node: &PySdfNode,
z: f32,
width: usize,
height: usize,
bounds: (f32, f32),
) -> PyResult<Bound<'py, PyArray3<u8>>> {
if width == 0 || height == 0 {
return Err(PyValueError::new_err("width and height must be > 0"));
}
let (min_b, max_b) = bounds;
let span = max_b - min_b;
if span <= 0.0 {
return Err(PyValueError::new_err(
"bounds.1 (max) must be > bounds.0 (min)",
));
}
let node_ref = &node.inner;
let mut points: Vec<Vec3> = Vec::with_capacity(width * height);
for j in 0..height {
let ty = (j as f32 + 0.5) / height as f32;
let y = min_b + ty * span;
for i in 0..width {
let tx = (i as f32 + 0.5) / width as f32;
let x = min_b + tx * span;
points.push(Vec3::new(x, y, z));
}
}
let distances = py.detach(|| eval_batch_parallel(node_ref, &points));
let contour_eps = span / (width.max(height) as f32);
let mut rgba: Vec<u8> = Vec::with_capacity(width * height * 4);
for &d in &distances {
let (r, g, b, a) = if d.abs() < contour_eps {
(255u8, 255u8, 255u8, 255u8)
} else if d <= 0.0 {
let t = (-d / span).clamp(0.0, 1.0);
let v = (255.0 * (1.0 - 0.7 * t)) as u8;
(0, 0, v, 255)
} else {
let t = (d / span).clamp(0.0, 1.0);
let r_ch = (255.0 * (1.0 - t)) as u8;
let g_ch = (255.0 * t) as u8;
(r_ch, g_ch, 0, 255)
};
rgba.push(r);
rgba.push(g);
rgba.push(b);
rgba.push(a);
}
let arr = Array3::<u8>::from_shape_vec((height, width, 4), rgba)
.map_err(|e| PyValueError::new_err(format!("shape error: {}", e)))?;
Ok(arr.into_pyarray(py))
}