use godot::classes::{
ArrayMesh, CollisionShape3D, ConvexPolygonShape3D, IMeshInstance3D, INode3D, IResource,
MeshInstance3D, Node3D, Resource,
};
use godot::prelude::*;
use std::sync::Arc;
use crate::compiled::{eval_compiled_batch_parallel, CompiledSdf};
use crate::mesh::{sdf_to_mesh, MarchingCubesConfig};
use crate::types::SdfNode;
struct AliceSdfExtension;
#[gdextension]
unsafe impl ExtensionLibrary for AliceSdfExtension {}
#[derive(GodotClass)]
#[class(base=Resource, tool)]
pub struct AliceSdfResource {
base: Base<Resource>,
#[var]
shape_type: GString,
#[var]
shape_params_json: GString,
sdf_cache: Option<SdfNode>,
}
#[godot_api]
impl IResource for AliceSdfResource {
fn init(base: Base<Resource>) -> Self {
Self {
base,
shape_type: "sphere".into(),
shape_params_json: r#"{"radius":1.0}"#.into(),
sdf_cache: None,
}
}
}
#[godot_api]
impl AliceSdfResource {
#[func]
fn set_shape(&mut self, shape_type: GString, params_json: GString) {
self.shape_type = shape_type;
self.shape_params_json = params_json;
self.sdf_cache = None; }
#[func]
fn build_sdf(&mut self) -> bool {
let shape = self.shape_type.to_string();
let params = self.shape_params_json.to_string();
let node = match build_sdf_from_params(&shape, ¶ms) {
Some(n) => n,
None => return false,
};
self.sdf_cache = Some(node);
true
}
#[func]
fn get_available_shapes() -> PackedStringArray {
let shapes = vec![
"sphere",
"box",
"cylinder",
"torus",
"plane",
"capsule",
"cone",
"ellipsoid",
"rounded_cone",
"pyramid",
"octahedron",
"hex_prism",
"tetrahedron",
"dodecahedron",
"icosahedron",
"truncated_octahedron",
"truncated_icosahedron",
"link",
"triangle",
"bezier",
"rounded_box",
"capped_cone",
"capped_torus",
"rounded_cylinder",
"triangular_prism",
"cut_sphere",
"cut_hollow_sphere",
"death_star",
"solid_angle",
"rhombus",
"horseshoe",
"vesica",
"heart",
"tube",
"barrel",
"diamond",
"egg",
"moon",
"cross_shape",
"blobby_cross",
"gyroid",
"schwarz_p",
"diamond_surface",
"neovius",
"lidinoid",
"iwp",
"frd",
"fischer_koch_s",
"pmy",
"chamfered_cube",
"superellipsoid",
"rounded_x",
"pie",
"trapezoid",
"parallelogram",
"tunnel",
"uneven_capsule",
"arc_shape",
"parabola_segment",
"regular_polygon",
"star_polygon",
"stairs",
"helix",
"box_frame",
"infinite_cylinder",
"infinite_cone",
"circle_2d",
"rect_2d",
"segment_2d",
"polygon_2d",
"rounded_rect_2d",
"annular_2d",
];
let mut arr = PackedStringArray::new();
for s in shapes {
arr.push(&GString::from(s));
}
arr
}
}
#[derive(GodotClass)]
#[class(base=Node3D, tool)]
pub struct AliceSdfNode {
base: Base<Node3D>,
#[var]
shape: GString,
#[var]
radius: f32,
#[var]
half_extents: Vector3,
#[var]
height: f32,
#[var]
smooth_k: f32,
sdf_node: Option<SdfNode>,
}
#[godot_api]
impl INode3D for AliceSdfNode {
fn init(base: Base<Node3D>) -> Self {
Self {
base,
shape: "sphere".into(),
radius: 1.0,
half_extents: Vector3::new(0.5, 0.5, 0.5),
height: 2.0,
smooth_k: 0.0,
sdf_node: None,
}
}
}
#[godot_api]
impl AliceSdfNode {
#[func]
fn build(&mut self) -> bool {
let node = match self.shape.to_string().as_str() {
"sphere" => SdfNode::sphere(self.radius),
"box" => SdfNode::box3d(
self.half_extents.x * 2.0,
self.half_extents.y * 2.0,
self.half_extents.z * 2.0,
),
"cylinder" => SdfNode::cylinder(self.radius, self.height),
"torus" => SdfNode::torus(self.radius, self.radius * 0.3),
"capsule" => {
let a = glam::Vec3::new(0.0, -self.height * 0.5, 0.0);
let b = glam::Vec3::new(0.0, self.height * 0.5, 0.0);
SdfNode::capsule(a, b, self.radius)
}
"cone" => SdfNode::cone(self.radius, self.height),
"ellipsoid" => SdfNode::ellipsoid(
self.half_extents.x,
self.half_extents.y,
self.half_extents.z,
),
"rounded_box" => SdfNode::rounded_box(
self.half_extents.x,
self.half_extents.y,
self.half_extents.z,
self.radius * 0.1,
),
"pyramid" => SdfNode::pyramid(self.height),
"octahedron" => SdfNode::octahedron(self.radius),
"hex_prism" => SdfNode::hex_prism(self.radius, self.height),
"gyroid" => SdfNode::gyroid(self.radius, 0.1),
"schwarz_p" => SdfNode::schwarz_p(self.radius, 0.1),
"heart" => SdfNode::heart(self.radius),
"tube" => SdfNode::tube(self.radius, self.radius * 0.2, self.height),
"barrel" => SdfNode::barrel(self.radius, self.height, self.radius * 0.1),
"diamond" => SdfNode::diamond(self.radius, self.height),
"helix" => SdfNode::helix(
self.radius,
self.radius * 0.2,
self.height * 0.5,
self.height,
),
"superellipsoid" => SdfNode::superellipsoid(
self.half_extents.x * 2.0,
self.half_extents.y * 2.0,
self.half_extents.z * 2.0,
0.5,
0.5,
),
"star_polygon" => SdfNode::star_polygon(self.radius, 5, self.radius * 0.5, self.height),
_ => return false,
};
self.sdf_node = Some(node);
true
}
#[func]
fn boolean_union(&mut self, other: Gd<AliceSdfNode>) {
let other_sdf = {
let other_ref = other.bind();
other_ref.sdf_node.clone()
};
if let (Some(a), Some(b)) = (self.sdf_node.take(), other_sdf) {
self.sdf_node = Some(if self.smooth_k > 0.0 {
a.smooth_union(b, self.smooth_k)
} else {
a.union(b)
});
}
}
#[func]
fn boolean_subtract(&mut self, other: Gd<AliceSdfNode>) {
let other_sdf = {
let other_ref = other.bind();
other_ref.sdf_node.clone()
};
if let (Some(a), Some(b)) = (self.sdf_node.take(), other_sdf) {
self.sdf_node = Some(if self.smooth_k > 0.0 {
a.smooth_subtract(b, self.smooth_k)
} else {
a.subtract(b)
});
}
}
#[func]
fn boolean_intersect(&mut self, other: Gd<AliceSdfNode>) {
let other_sdf = {
let other_ref = other.bind();
other_ref.sdf_node.clone()
};
if let (Some(a), Some(b)) = (self.sdf_node.take(), other_sdf) {
self.sdf_node = Some(if self.smooth_k > 0.0 {
a.smooth_intersection(b, self.smooth_k)
} else {
a.intersection(b)
});
}
}
#[func]
fn eval_distance(&self, point: Vector3) -> f32 {
match &self.sdf_node {
Some(node) => crate::eval::eval(node, glam::Vec3::new(point.x, point.y, point.z)),
None => f32::MAX,
}
}
#[func]
fn apply_twist(&mut self, strength: f32) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.twist(strength));
}
}
#[func]
fn apply_bend(&mut self, curvature: f32) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.bend(curvature));
}
}
#[func]
fn apply_mirror(&mut self, axis: Vector3) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.mirror(axis.x > 0.5, axis.y > 0.5, axis.z > 0.5));
}
}
#[func]
fn apply_onion(&mut self, thickness: f32) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.onion(thickness));
}
}
#[func]
fn apply_round(&mut self, radius: f32) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.round(radius));
}
}
#[func]
fn apply_icosahedral_symmetry(&mut self) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.icosahedral_symmetry());
}
}
#[func]
fn apply_surface_roughness(&mut self, frequency: f32, amplitude: f32, octaves: i32) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node =
Some(node.surface_roughness(frequency, amplitude, octaves.max(1) as u32));
}
}
#[func]
fn apply_noise(&mut self, amplitude: f32, frequency: f32, seed: i32) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.noise(amplitude, frequency, seed as u32));
}
}
#[func]
fn apply_polar_repeat(&mut self, count: i32) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.polar_repeat(count.max(1) as u32));
}
}
#[func]
fn apply_octant_mirror(&mut self) {
if let Some(node) = self.sdf_node.take() {
self.sdf_node = Some(node.octant_mirror());
}
}
#[func]
fn to_glsl(&self) -> GString {
#[cfg(feature = "glsl")]
{
use crate::compiled::glsl::{GlslShader, GlslTranspileMode};
match &self.sdf_node {
Some(node) => {
GString::from(GlslShader::transpile(node, GlslTranspileMode::Hardcoded).source)
}
None => GString::new(),
}
}
#[cfg(not(feature = "glsl"))]
{
GString::from("GLSL feature not enabled")
}
}
}
#[derive(GodotClass)]
#[class(base=MeshInstance3D, tool)]
pub struct AliceSdfMeshInstance {
base: Base<MeshInstance3D>,
#[var]
resolution: i32,
#[var]
bounds_size: f32,
#[var]
auto_rebuild: bool,
cached_sdf: Option<SdfNode>,
}
#[godot_api]
impl IMeshInstance3D for AliceSdfMeshInstance {
fn init(base: Base<MeshInstance3D>) -> Self {
Self {
base,
resolution: 32,
bounds_size: 2.0,
auto_rebuild: true,
cached_sdf: None,
}
}
}
#[godot_api]
impl AliceSdfMeshInstance {
#[func]
fn set_sdf_from_node(&mut self, node: Gd<AliceSdfNode>) {
let sdf = {
let node_ref = node.bind();
node_ref.sdf_node.clone()
};
self.cached_sdf = sdf;
if self.auto_rebuild {
self.rebuild_mesh();
}
}
#[func]
fn set_sdf_from_resource(&mut self, mut resource: Gd<AliceSdfResource>) {
{
let mut res = resource.bind_mut();
res.build_sdf();
}
let sdf = {
let res = resource.bind();
res.sdf_cache.clone()
};
self.cached_sdf = sdf;
if self.auto_rebuild {
self.rebuild_mesh();
}
}
#[func]
fn rebuild_mesh(&mut self) {
let sdf = match &self.cached_sdf {
Some(s) => s,
None => return,
};
let bounds = self.bounds_size;
let min_bounds = glam::Vec3::splat(-bounds);
let max_bounds = glam::Vec3::splat(bounds);
let config = MarchingCubesConfig {
resolution: self.resolution.max(4) as usize,
..Default::default()
};
let mesh = sdf_to_mesh(sdf, min_bounds, max_bounds, &config);
if mesh.vertices.is_empty() {
return;
}
let godot_mesh = alice_mesh_to_godot(&mesh);
self.base_mut()
.set_mesh(&godot_mesh.upcast::<godot::classes::Mesh>());
}
#[func]
fn get_vertex_count(&self) -> i32 {
if let Some(mesh) = self.base().get_mesh() {
if let Ok(array_mesh) = mesh.try_cast::<ArrayMesh>() {
let mut total = 0i32;
for i in 0..array_mesh.get_surface_count() {
let arrays = array_mesh.surface_get_arrays(i);
if let Some(verts) = arrays.get(0) {
if let Ok(packed) = verts.try_to::<PackedVector3Array>() {
total += packed.len() as i32;
}
}
}
return total;
}
}
0
}
}
fn alice_mesh_to_godot(mesh: &crate::mesh::Mesh) -> Gd<ArrayMesh> {
use godot::classes::mesh::PrimitiveType;
use godot::classes::rendering_server::ArrayType;
let mut array_mesh = ArrayMesh::new_gd();
let vert_count = mesh.vertices.len();
let mut positions = PackedVector3Array::new();
let mut normals = PackedVector3Array::new();
let mut uvs = PackedVector2Array::new();
let mut indices = PackedInt32Array::new();
positions.resize(vert_count);
normals.resize(vert_count);
uvs.resize(vert_count);
for (i, v) in mesh.vertices.iter().enumerate() {
positions[i] = Vector3::new(v.position.x, v.position.y, v.position.z);
normals[i] = Vector3::new(v.normal.x, v.normal.y, v.normal.z);
uvs[i] = Vector2::new(v.uv.x, v.uv.y);
}
indices.resize(mesh.indices.len());
for (i, &idx) in mesh.indices.iter().enumerate() {
indices[i] = idx as i32;
}
let mut arrays = VariantArray::new();
arrays.resize(ArrayType::MAX.ord() as usize, &Variant::nil());
arrays.set(ArrayType::VERTEX.ord() as usize, &positions.to_variant());
arrays.set(ArrayType::NORMAL.ord() as usize, &normals.to_variant());
arrays.set(ArrayType::TEX_UV.ord() as usize, &uvs.to_variant());
arrays.set(ArrayType::INDEX.ord() as usize, &indices.to_variant());
array_mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &arrays);
array_mesh
}
fn build_sdf_from_params(shape: &str, params_json: &str) -> Option<SdfNode> {
let params: serde_json::Value = serde_json::from_str(params_json).ok()?;
match shape {
"sphere" => {
let radius = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::sphere(radius))
}
"box" => {
let w = params.get("width")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
let d = params.get("depth")?.as_f64()? as f32;
Some(SdfNode::box3d(w, h, d))
}
"cylinder" => {
let r = params.get("radius")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::cylinder(r, h))
}
"torus" => {
let major = params.get("major_radius")?.as_f64()? as f32;
let minor = params.get("minor_radius")?.as_f64()? as f32;
Some(SdfNode::torus(major, minor))
}
"plane" => {
let nx = params
.get("normal_x")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let ny = params
.get("normal_y")
.and_then(|v| v.as_f64())
.unwrap_or(1.0) as f32;
let nz = params
.get("normal_z")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let dist = params
.get("distance")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
Some(SdfNode::plane(glam::Vec3::new(nx, ny, nz), dist))
}
"capsule" => {
let ax = params.get("ax").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
let ay = params.get("ay").and_then(|v| v.as_f64()).unwrap_or(-1.0) as f32;
let az = params.get("az").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
let bx = params.get("bx").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
let by = params.get("by").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let bz = params.get("bz").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
let r = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::capsule(
glam::Vec3::new(ax, ay, az),
glam::Vec3::new(bx, by, bz),
r,
))
}
"cone" => {
let r = params.get("radius")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::cone(r, h))
}
"ellipsoid" => {
let rx = params.get("rx")?.as_f64()? as f32;
let ry = params.get("ry")?.as_f64()? as f32;
let rz = params.get("rz")?.as_f64()? as f32;
Some(SdfNode::ellipsoid(rx, ry, rz))
}
"rounded_cone" => {
let r1 = params.get("r1")?.as_f64()? as f32;
let r2 = params.get("r2")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::rounded_cone(r1, r2, h))
}
"pyramid" => {
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::pyramid(h))
}
"octahedron" => {
let size = params.get("size")?.as_f64()? as f32;
Some(SdfNode::octahedron(size))
}
"hex_prism" => {
let hex_radius = params.get("hex_radius")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::hex_prism(hex_radius, h))
}
"link" => {
let length = params.get("length")?.as_f64()? as f32;
let r1 = params.get("r1")?.as_f64()? as f32;
let r2 = params.get("r2")?.as_f64()? as f32;
Some(SdfNode::link(length, r1, r2))
}
"triangle" => {
let ax = params.get("ax")?.as_f64()? as f32;
let ay = params.get("ay")?.as_f64()? as f32;
let az = params.get("az")?.as_f64()? as f32;
let bx = params.get("bx")?.as_f64()? as f32;
let by = params.get("by")?.as_f64()? as f32;
let bz = params.get("bz")?.as_f64()? as f32;
let cx = params.get("cx")?.as_f64()? as f32;
let cy = params.get("cy")?.as_f64()? as f32;
let cz = params.get("cz")?.as_f64()? as f32;
Some(SdfNode::triangle(
glam::Vec3::new(ax, ay, az),
glam::Vec3::new(bx, by, bz),
glam::Vec3::new(cx, cy, cz),
))
}
"bezier" => {
let ax = params.get("ax")?.as_f64()? as f32;
let ay = params.get("ay")?.as_f64()? as f32;
let az = params.get("az")?.as_f64()? as f32;
let bx = params.get("bx")?.as_f64()? as f32;
let by = params.get("by")?.as_f64()? as f32;
let bz = params.get("bz")?.as_f64()? as f32;
let cx = params.get("cx")?.as_f64()? as f32;
let cy = params.get("cy")?.as_f64()? as f32;
let cz = params.get("cz")?.as_f64()? as f32;
let r = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::bezier(
glam::Vec3::new(ax, ay, az),
glam::Vec3::new(bx, by, bz),
glam::Vec3::new(cx, cy, cz),
r,
))
}
"rounded_box" => {
let hx = params.get("hx")?.as_f64()? as f32;
let hy = params.get("hy")?.as_f64()? as f32;
let hz = params.get("hz")?.as_f64()? as f32;
let rr = params
.get("round_radius")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::rounded_box(hx, hy, hz, rr))
}
"capped_cone" => {
let h = params.get("height")?.as_f64()? as f32;
let r1 = params.get("r1")?.as_f64()? as f32;
let r2 = params.get("r2")?.as_f64()? as f32;
Some(SdfNode::capped_cone(h, r1, r2))
}
"capped_torus" => {
let major = params.get("major_radius")?.as_f64()? as f32;
let minor = params.get("minor_radius")?.as_f64()? as f32;
let cap = params
.get("cap_angle")
.and_then(|v| v.as_f64())
.unwrap_or(1.57) as f32;
Some(SdfNode::capped_torus(major, minor, cap))
}
"rounded_cylinder" => {
let r = params.get("radius")?.as_f64()? as f32;
let rr = params
.get("round_radius")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::rounded_cylinder(r, rr, h))
}
"triangular_prism" => {
let w = params.get("width")?.as_f64()? as f32;
let d = params.get("depth")?.as_f64()? as f32;
Some(SdfNode::triangular_prism(w, d))
}
"cut_sphere" => {
let r = params.get("radius")?.as_f64()? as f32;
let ch = params.get("cut_height")?.as_f64()? as f32;
Some(SdfNode::cut_sphere(r, ch))
}
"cut_hollow_sphere" => {
let r = params.get("radius")?.as_f64()? as f32;
let ch = params.get("cut_height")?.as_f64()? as f32;
let t = params.get("thickness")?.as_f64()? as f32;
Some(SdfNode::cut_hollow_sphere(r, ch, t))
}
"death_star" => {
let ra = params.get("ra")?.as_f64()? as f32;
let rb = params.get("rb")?.as_f64()? as f32;
let d = params.get("d")?.as_f64()? as f32;
Some(SdfNode::death_star(ra, rb, d))
}
"solid_angle" => {
let angle = params.get("angle")?.as_f64()? as f32;
let r = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::solid_angle(angle, r))
}
"rhombus" => {
let la = params.get("la")?.as_f64()? as f32;
let lb = params.get("lb")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
let rr = params
.get("round_radius")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
Some(SdfNode::rhombus(la, lb, h, rr))
}
"horseshoe" => {
let angle = params.get("angle")?.as_f64()? as f32;
let r = params.get("radius")?.as_f64()? as f32;
let length = params.get("length")?.as_f64()? as f32;
let w = params.get("width")?.as_f64()? as f32;
let t = params.get("thickness")?.as_f64()? as f32;
Some(SdfNode::horseshoe(angle, r, length, w, t))
}
"vesica" => {
let r = params.get("radius")?.as_f64()? as f32;
let dist = params.get("dist")?.as_f64()? as f32;
Some(SdfNode::vesica(r, dist))
}
"infinite_cylinder" => {
let r = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::infinite_cylinder(r))
}
"infinite_cone" => {
let angle = params.get("angle")?.as_f64()? as f32;
Some(SdfNode::infinite_cone(angle))
}
"heart" => {
let size = params.get("size")?.as_f64()? as f32;
Some(SdfNode::heart(size))
}
"tube" => {
let outer_r = params.get("outer_radius")?.as_f64()? as f32;
let t = params.get("thickness")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::tube(outer_r, t, h))
}
"barrel" => {
let r = params.get("radius")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
let bulge = params.get("bulge").and_then(|v| v.as_f64()).unwrap_or(0.1) as f32;
Some(SdfNode::barrel(r, h, bulge))
}
"diamond" => {
let r = params.get("radius")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::diamond(r, h))
}
"chamfered_cube" => {
let w = params.get("width")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
let d = params.get("depth")?.as_f64()? as f32;
let chamfer = params
.get("chamfer")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::chamfered_cube(w, h, d, chamfer))
}
"egg" => {
let ra = params.get("ra")?.as_f64()? as f32;
let rb = params.get("rb")?.as_f64()? as f32;
Some(SdfNode::egg(ra, rb))
}
"moon" => {
let d = params.get("d")?.as_f64()? as f32;
let ra = params.get("ra")?.as_f64()? as f32;
let rb = params.get("rb")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::moon(d, ra, rb, h))
}
"cross_shape" => {
let length = params.get("length")?.as_f64()? as f32;
let t = params.get("thickness")?.as_f64()? as f32;
let rr = params
.get("round_radius")
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::cross_shape(length, t, rr, h))
}
"blobby_cross" => {
let size = params.get("size")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::blobby_cross(size, h))
}
"gyroid" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::gyroid(scale, thickness))
}
"schwarz_p" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::schwarz_p(scale, thickness))
}
"diamond_surface" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::diamond_surface(scale, thickness))
}
"neovius" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::neovius(scale, thickness))
}
"lidinoid" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::lidinoid(scale, thickness))
}
"iwp" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::iwp(scale, thickness))
}
"frd" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::frd(scale, thickness))
}
"fischer_koch_s" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::fischer_koch_s(scale, thickness))
}
"pmy" => {
let scale = params.get("scale").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let thickness = params
.get("thickness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
Some(SdfNode::pmy(scale, thickness))
}
"superellipsoid" => {
let w = params.get("width")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
let d = params.get("depth")?.as_f64()? as f32;
let e1 = params.get("e1").and_then(|v| v.as_f64()).unwrap_or(0.5) as f32;
let e2 = params.get("e2").and_then(|v| v.as_f64()).unwrap_or(0.5) as f32;
Some(SdfNode::superellipsoid(w, h, d, e1, e2))
}
"rounded_x" => {
let w = params.get("width")?.as_f64()? as f32;
let rr = params
.get("round_radius")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::rounded_x(w, rr, h))
}
"pie" => {
let angle = params.get("angle")?.as_f64()? as f32;
let r = params.get("radius")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::pie(angle, r, h))
}
"trapezoid" => {
let r1 = params.get("r1")?.as_f64()? as f32;
let r2 = params.get("r2")?.as_f64()? as f32;
let trap_height = params.get("trap_height")?.as_f64()? as f32;
let d = params.get("depth")?.as_f64()? as f32;
Some(SdfNode::trapezoid(r1, r2, trap_height, d))
}
"parallelogram" => {
let w = params.get("width")?.as_f64()? as f32;
let para_height = params.get("para_height")?.as_f64()? as f32;
let skew = params.get("skew").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
let d = params.get("depth")?.as_f64()? as f32;
Some(SdfNode::parallelogram(w, para_height, skew, d))
}
"tunnel" => {
let w = params.get("width")?.as_f64()? as f32;
let height_2d = params.get("height_2d")?.as_f64()? as f32;
let d = params.get("depth")?.as_f64()? as f32;
Some(SdfNode::tunnel(w, height_2d, d))
}
"uneven_capsule" => {
let r1 = params.get("r1")?.as_f64()? as f32;
let r2 = params.get("r2")?.as_f64()? as f32;
let cap_height = params.get("cap_height")?.as_f64()? as f32;
let d = params.get("depth")?.as_f64()? as f32;
Some(SdfNode::uneven_capsule(r1, r2, cap_height, d))
}
"arc_shape" => {
let aperture = params.get("aperture")?.as_f64()? as f32;
let r = params.get("radius")?.as_f64()? as f32;
let t = params.get("thickness")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::arc_shape(aperture, r, t, h))
}
"parabola_segment" => {
let w = params.get("width")?.as_f64()? as f32;
let para_height = params.get("para_height")?.as_f64()? as f32;
let d = params.get("depth")?.as_f64()? as f32;
Some(SdfNode::parabola_segment(w, para_height, d))
}
"regular_polygon" => {
let r = params.get("radius")?.as_f64()? as f32;
let n_sides = params
.get("n_sides")
.and_then(|v| v.as_f64())
.unwrap_or(6.0) as u32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::regular_polygon(r, n_sides, h))
}
"star_polygon" => {
let r = params.get("radius")?.as_f64()? as f32;
let n_points = params
.get("n_points")
.and_then(|v| v.as_f64())
.unwrap_or(5.0) as u32;
let m = params.get("m").and_then(|v| v.as_f64()).unwrap_or(0.5) as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::star_polygon(r, n_points, m, h))
}
"stairs" => {
let step_width = params.get("step_width")?.as_f64()? as f32;
let step_height = params.get("step_height")?.as_f64()? as f32;
let n_steps = params
.get("n_steps")
.and_then(|v| v.as_f64())
.unwrap_or(5.0) as u32;
let d = params.get("depth")?.as_f64()? as f32;
Some(SdfNode::stairs(step_width, step_height, n_steps, d))
}
"helix" => {
let major_r = params.get("major_r")?.as_f64()? as f32;
let minor_r = params.get("minor_r")?.as_f64()? as f32;
let pitch = params.get("pitch")?.as_f64()? as f32;
let h = params.get("height")?.as_f64()? as f32;
Some(SdfNode::helix(major_r, minor_r, pitch, h))
}
"tetrahedron" => {
let size = params.get("size")?.as_f64()? as f32;
Some(SdfNode::tetrahedron(size))
}
"dodecahedron" => {
let r = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::dodecahedron(r))
}
"icosahedron" => {
let r = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::icosahedron(r))
}
"truncated_octahedron" => {
let r = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::truncated_octahedron(r))
}
"truncated_icosahedron" => {
let r = params.get("radius")?.as_f64()? as f32;
Some(SdfNode::truncated_icosahedron(r))
}
"box_frame" => {
let hx = params.get("hx")?.as_f64()? as f32;
let hy = params.get("hy")?.as_f64()? as f32;
let hz = params.get("hz")?.as_f64()? as f32;
let edge = params.get("edge").and_then(|v| v.as_f64()).unwrap_or(0.1) as f32;
Some(SdfNode::box_frame(glam::Vec3::new(hx, hy, hz), edge))
}
"circle_2d" => {
let r = params.get("radius")?.as_f64()? as f32;
let hh = params.get("half_height")?.as_f64()? as f32;
Some(SdfNode::circle_2d(r, hh))
}
"rect_2d" => {
let hw = params.get("half_w")?.as_f64()? as f32;
let hh_rect = params.get("half_h")?.as_f64()? as f32;
let hh = params.get("half_height")?.as_f64()? as f32;
Some(SdfNode::rect_2d(hw, hh_rect, hh))
}
"segment_2d" => {
let ax = params.get("ax")?.as_f64()? as f32;
let ay = params.get("ay")?.as_f64()? as f32;
let bx = params.get("bx")?.as_f64()? as f32;
let by = params.get("by")?.as_f64()? as f32;
let t = params.get("thickness")?.as_f64()? as f32;
let hh = params.get("half_height")?.as_f64()? as f32;
Some(SdfNode::segment_2d(ax, ay, bx, by, t, hh))
}
"polygon_2d" => {
let hh = params.get("half_height")?.as_f64()? as f32;
let verts = vec![
glam::Vec2::new(1.0, 0.0),
glam::Vec2::new(0.0, 1.0),
glam::Vec2::new(-1.0, 0.0),
];
Some(SdfNode::polygon_2d(verts, hh))
}
"rounded_rect_2d" => {
let hw = params.get("half_w")?.as_f64()? as f32;
let hh_rect = params.get("half_h")?.as_f64()? as f32;
let rr = params
.get("round_radius")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
let hh = params.get("half_height")?.as_f64()? as f32;
Some(SdfNode::rounded_rect_2d(hw, hh_rect, rr, hh))
}
"annular_2d" => {
let outer_r = params.get("outer_radius")?.as_f64()? as f32;
let t = params.get("thickness")?.as_f64()? as f32;
let hh = params.get("half_height")?.as_f64()? as f32;
Some(SdfNode::annular_2d(outer_r, t, hh))
}
_ => None,
}
}
#[derive(GodotClass)]
#[class(base=Node3D, tool)]
pub struct AliceSdfCollisionGenerator {
base: Base<Node3D>,
#[var]
collision_resolution: i32,
#[var]
bounds_size: f32,
#[var]
max_collision_vertices: i32,
}
#[godot_api]
impl INode3D for AliceSdfCollisionGenerator {
fn init(base: Base<Node3D>) -> Self {
Self {
base,
collision_resolution: 16,
bounds_size: 2.0,
max_collision_vertices: 256,
}
}
}
#[godot_api]
impl AliceSdfCollisionGenerator {
#[func]
fn generate_collision_shape(&self, sdf_node: Gd<AliceSdfNode>) -> Option<Gd<CollisionShape3D>> {
let sdf = {
let node_ref = sdf_node.bind();
node_ref.sdf_node.clone()
}?;
let bounds = self.bounds_size;
let min_bounds = glam::Vec3::splat(-bounds);
let max_bounds = glam::Vec3::splat(bounds);
let config = MarchingCubesConfig {
resolution: self.collision_resolution.max(4) as usize,
compute_normals: false, ..Default::default()
};
let mesh = sdf_to_mesh(&sdf, min_bounds, max_bounds, &config);
if mesh.vertices.is_empty() {
return None;
}
let mut points = PackedVector3Array::new();
let step = if mesh.vertices.len() > self.max_collision_vertices as usize {
mesh.vertices.len() / self.max_collision_vertices as usize
} else {
1
};
for (i, v) in mesh.vertices.iter().enumerate() {
if i % step == 0 {
points.push(Vector3::new(v.position.x, v.position.y, v.position.z));
}
}
let mut convex_shape = ConvexPolygonShape3D::new_gd();
convex_shape.set_points(&points);
let mut collision_shape = CollisionShape3D::new_alloc();
collision_shape.set_shape(&convex_shape.upcast::<godot::classes::Shape3D>());
Some(collision_shape)
}
}
#[derive(GodotClass)]
#[class(base=Node3D, tool)]
pub struct AliceSdfLodManager {
base: Base<Node3D>,
#[var]
num_lod_levels: i32,
#[var]
base_resolution: i32,
#[var]
lod0_distance: f32,
#[var]
distance_multiplier: f32,
#[var]
bounds_size: f32,
#[var]
current_lod: i32,
lod_meshes: Vec<Gd<ArrayMesh>>,
cached_sdf: Option<SdfNode>,
}
#[godot_api]
impl INode3D for AliceSdfLodManager {
fn init(base: Base<Node3D>) -> Self {
Self {
base,
num_lod_levels: 3,
base_resolution: 64,
lod0_distance: 10.0,
distance_multiplier: 2.0,
bounds_size: 2.0,
current_lod: 0,
lod_meshes: Vec::new(),
cached_sdf: None,
}
}
}
#[godot_api]
impl AliceSdfLodManager {
#[func]
fn set_sdf(&mut self, sdf_node: Gd<AliceSdfNode>) {
let sdf = {
let node_ref = sdf_node.bind();
node_ref.sdf_node.clone()
};
self.cached_sdf = sdf;
self.generate_all_lods();
}
#[func]
fn generate_all_lods(&mut self) {
let sdf = match &self.cached_sdf {
Some(s) => s,
None => return,
};
let bounds = self.bounds_size;
let min_bounds = glam::Vec3::splat(-bounds);
let max_bounds = glam::Vec3::splat(bounds);
self.lod_meshes.clear();
for level in 0..self.num_lod_levels {
let res = (self.base_resolution >> level).max(4) as usize;
let config = MarchingCubesConfig {
resolution: res,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(sdf, min_bounds, max_bounds, &config);
if !mesh.vertices.is_empty() {
self.lod_meshes.push(alice_mesh_to_godot(&mesh));
}
}
}
#[func]
fn update_lod(&mut self, camera_position: Vector3) -> i32 {
if self.lod_meshes.is_empty() {
return -1;
}
let self_pos = self.base().get_global_position();
let distance = self_pos.distance_to(camera_position);
let mut level = 0i32;
let mut threshold = self.lod0_distance;
while level < (self.lod_meshes.len() as i32 - 1) && distance > threshold {
level += 1;
threshold *= self.distance_multiplier;
}
self.current_lod = level;
level
}
#[func]
fn get_lod_mesh(&self, level: i32) -> Option<Gd<ArrayMesh>> {
self.lod_meshes.get(level as usize).cloned()
}
#[func]
fn get_lod_count(&self) -> i32 {
self.lod_meshes.len() as i32
}
#[func]
fn get_lod_vertex_counts(&self) -> PackedInt32Array {
let mut counts = PackedInt32Array::new();
for mesh in &self.lod_meshes {
let mut total = 0i32;
for i in 0..mesh.get_surface_count() {
let arrays = mesh.surface_get_arrays(i);
if let Some(verts) = arrays.get(0) {
if let Ok(packed) = verts.try_to::<PackedVector3Array>() {
total += packed.len() as i32;
}
}
}
counts.push(total);
}
counts
}
}
#[derive(GodotClass)]
#[class(base=RefCounted)]
pub struct AliceSdfBatchEvaluator {
base: Base<RefCounted>,
compiled: Option<Arc<CompiledSdf>>,
}
#[godot_api]
impl IRefCounted for AliceSdfBatchEvaluator {
fn init(base: Base<RefCounted>) -> Self {
Self {
base,
compiled: None,
}
}
}
#[godot_api]
impl AliceSdfBatchEvaluator {
#[func]
fn compile(&mut self, sdf_node: Gd<AliceSdfNode>) -> bool {
let sdf = {
let node_ref = sdf_node.bind();
node_ref.sdf_node.clone()
};
match sdf {
Some(node) => {
self.compiled = Some(Arc::new(CompiledSdf::compile(&node)));
true
}
None => false,
}
}
#[func]
fn eval_batch(&self, points: PackedVector3Array) -> PackedFloat32Array {
let compiled = match &self.compiled {
Some(c) => c,
None => return PackedFloat32Array::new(),
};
let len = points.len();
let mut glam_points = Vec::with_capacity(len);
for i in 0..len {
let p = points[i];
glam_points.push(glam::Vec3::new(p.x, p.y, p.z));
}
let distances = eval_compiled_batch_parallel(compiled, &glam_points);
let mut result = PackedFloat32Array::new();
result.resize(len);
for (i, &d) in distances.iter().enumerate() {
result[i] = d;
}
result
}
#[func]
fn is_compiled(&self) -> bool {
self.compiled.is_some()
}
#[func]
fn get_instruction_count(&self) -> i32 {
self.compiled
.as_ref()
.map(|c| c.instruction_count() as i32)
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::build_sdf_from_params;
use crate::eval::eval;
use crate::types::SdfNode;
use glam::Vec3;
const ADVERTISED: [&str; 72] = [
"sphere",
"box",
"cylinder",
"torus",
"plane",
"capsule",
"cone",
"ellipsoid",
"rounded_cone",
"pyramid",
"octahedron",
"hex_prism",
"tetrahedron",
"dodecahedron",
"icosahedron",
"truncated_octahedron",
"truncated_icosahedron",
"link",
"triangle",
"bezier",
"rounded_box",
"capped_cone",
"capped_torus",
"rounded_cylinder",
"triangular_prism",
"cut_sphere",
"cut_hollow_sphere",
"death_star",
"solid_angle",
"rhombus",
"horseshoe",
"vesica",
"heart",
"tube",
"barrel",
"diamond",
"egg",
"moon",
"cross_shape",
"blobby_cross",
"gyroid",
"schwarz_p",
"diamond_surface",
"neovius",
"lidinoid",
"iwp",
"frd",
"fischer_koch_s",
"pmy",
"chamfered_cube",
"superellipsoid",
"rounded_x",
"pie",
"trapezoid",
"parallelogram",
"tunnel",
"uneven_capsule",
"arc_shape",
"parabola_segment",
"regular_polygon",
"star_polygon",
"stairs",
"helix",
"box_frame",
"infinite_cylinder",
"infinite_cone",
"circle_2d",
"rect_2d",
"segment_2d",
"polygon_2d",
"rounded_rect_2d",
"annular_2d",
];
fn every_param() -> String {
r#"{
"radius": 0.6, "width": 0.8, "height": 0.7, "depth": 0.5,
"major_radius": 0.7, "minor_radius": 0.2,
"normal_x": 0.0, "normal_y": 1.0, "normal_z": 0.0, "distance": 0.2,
"ax": -0.5, "ay": 0.0, "az": 0.0,
"bx": 0.5, "by": 0.2, "bz": 0.0,
"cx": 0.1, "cy": 0.6, "cz": -0.2,
"rx": 0.61, "ry": 0.43, "rz": 0.29,
"r1": 0.4, "r2": 0.2, "ra": 0.5, "rb": 0.3, "d": 0.35,
"size": 0.6, "hex_radius": 0.5, "length": 0.55,
"hx": 0.31, "hy": 0.47, "hz": 0.59, "round_radius": 0.07,
"cap_angle": 0.8, "cut_height": 0.2, "thickness": 0.1,
"angle": 0.9, "la": 0.4, "lb": 0.3,
"outer_radius": 0.5, "bulge": 0.15, "chamfer": 0.08,
"scale": 2.0, "e1": 0.7, "e2": 1.3,
"trap_height": 0.5, "para_height": 0.4, "skew": 0.2,
"height_2d": 0.45, "cap_height": 0.35, "aperture": 0.8,
"n_sides": 6.0, "n_points": 5.0, "m": 2.0,
"step_width": 0.2, "step_height": 0.15, "n_steps": 4.0,
"major_r": 0.5, "minor_r": 0.12, "pitch": 0.3, "edge": 0.05,
"half_w": 0.4, "half_h": 0.3, "half_height": 0.3, "dist": 0.25
}"#
.to_string()
}
#[test]
fn every_advertised_shape_builds_and_evaluates() {
let params = every_param();
let pts = [
Vec3::ZERO,
Vec3::new(0.25, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(-0.7, 0.3, 1.1),
Vec3::new(2.5, -2.5, 2.5),
];
let mut failures = Vec::new();
for name in ADVERTISED {
match build_sdf_from_params(name, ¶ms) {
None => failures.push(format!("{name}: build_sdf_from_params returned None")),
Some(node) => {
for p in pts {
let d = eval(&node, p);
if !d.is_finite() {
failures.push(format!("{name} at {p:?}: eval returned {d}"));
break;
}
}
}
}
}
assert!(
failures.is_empty(),
"{} advertised shape(s) unusable:\n{}",
failures.len(),
failures.join("\n")
);
}
#[test]
fn documented_shape_params_match_closed_forms() {
let build = |shape: &str, json: &str| {
build_sdf_from_params(shape, json).unwrap_or_else(|| panic!("{shape} did not build"))
};
let s = build("sphere", r#"{"radius": 1.0}"#);
assert_eq!(eval(&s, Vec3::new(0.5, 0.0, 0.0)), -0.5, "sphere inside");
assert_eq!(eval(&s, Vec3::new(2.0, 0.0, 0.0)), 1.0, "sphere outside");
let b = build("box", r#"{"width": 2.0, "height": 2.0, "depth": 2.0}"#);
assert_eq!(eval(&b, Vec3::ZERO), -1.0, "box centre");
assert_eq!(eval(&b, Vec3::new(2.0, 0.0, 0.0)), 1.0, "box face");
let corner = eval(&b, Vec3::new(2.0, 2.0, 2.0));
assert!(
(corner - 3.0_f32.sqrt()).abs() < 1e-6,
"box corner {corner} vs sqrt(3)"
);
let b2 = build("box", r#"{"width": 2.0, "height": 1.0, "depth": 0.5}"#);
assert_eq!(eval(&b2, Vec3::ZERO), -0.25, "flat box centre");
assert_eq!(eval(&b2, Vec3::new(2.0, 0.0, 0.0)), 1.0, "flat box +X face");
assert_eq!(eval(&b2, Vec3::new(0.0, 1.5, 0.0)), 1.0, "flat box +Y face");
assert_eq!(
eval(&b2, Vec3::new(0.0, 0.0, 0.75)),
0.5,
"flat box +Z face"
);
let c = build("cylinder", r#"{"radius": 1.0, "height": 2.0}"#);
assert_eq!(eval(&c, Vec3::ZERO), -1.0, "cylinder centre");
assert_eq!(eval(&c, Vec3::new(2.0, 0.0, 0.0)), 1.0, "cylinder side");
assert_eq!(eval(&c, Vec3::new(0.0, 2.0, 0.0)), 1.0, "cylinder cap");
let t = build("torus", r#"{"major_radius": 1.0, "minor_radius": 0.25}"#);
assert_eq!(eval(&t, Vec3::new(1.0, 0.0, 0.0)), -0.25, "torus tube");
assert_eq!(eval(&t, Vec3::new(1.5, 0.0, 0.0)), 0.25, "torus outside");
assert_eq!(eval(&t, Vec3::ZERO), 0.75, "torus hole");
let pl = build("plane", "{}");
assert_eq!(eval(&pl, Vec3::new(0.0, 3.0, 0.0)), 3.0, "default plane");
let pl2 = build(
"plane",
r#"{"normal_x": 1.0, "normal_y": 0.0, "distance": 0.5}"#,
);
assert_eq!(eval(&pl2, Vec3::new(2.0, 0.0, 0.0)), 1.5, "plane +X at 0.5");
let cap = build(
"capsule",
r#"{"ax":0.0,"ay":-1.0,"az":0.0,"bx":0.0,"by":1.0,"bz":0.0,"radius":0.5}"#,
);
assert_eq!(eval(&cap, Vec3::ZERO), -0.5, "capsule axis");
assert_eq!(eval(&cap, Vec3::new(1.0, 0.0, 0.0)), 0.5, "capsule side");
assert_eq!(eval(&cap, Vec3::new(0.0, 2.0, 0.0)), 0.5, "capsule cap");
let e = build("ellipsoid", r#"{"rx": 1.0, "ry": 0.5, "rz": 0.25}"#);
assert!(
eval(&e, Vec3::new(1.0, 0.0, 0.0)).abs() < 1e-6,
"ellipsoid +X"
);
assert!(
eval(&e, Vec3::new(0.0, 0.5, 0.0)).abs() < 1e-6,
"ellipsoid +Y"
);
assert!(
eval(&e, Vec3::new(0.0, 0.0, 0.25)).abs() < 1e-6,
"ellipsoid +Z"
);
assert!(
eval(&e, Vec3::new(0.0, 1.0, 0.0)) > 0.1,
"ellipsoid is not a sphere"
);
}
#[test]
fn shape_params_forward_to_the_named_constructor() {
let pts: Vec<Vec3> = {
let mut state: u64 = 0x2545_f491_4f6c_dd1d;
let mut next = move || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((state >> 40) as f32) / ((1u64 << 24) as f32) * 6.0 - 3.0
};
(0..64).map(|_| Vec3::new(next(), next(), next())).collect()
};
let cases: Vec<(&str, &str, SdfNode)> = vec![
("sphere", r#"{"radius": 0.31}"#, SdfNode::sphere(0.31)),
(
"ellipsoid",
r#"{"rx": 0.61, "ry": 0.43, "rz": 0.29}"#,
SdfNode::ellipsoid(0.61, 0.43, 0.29),
),
("octahedron", r#"{"size": 0.6}"#, SdfNode::octahedron(0.6)),
("tetrahedron", r#"{"size": 0.6}"#, SdfNode::tetrahedron(0.6)),
(
"dodecahedron",
r#"{"radius": 0.6}"#,
SdfNode::dodecahedron(0.6),
),
(
"icosahedron",
r#"{"radius": 0.6}"#,
SdfNode::icosahedron(0.6),
),
(
"rounded_box",
r#"{"hx": 0.31, "hy": 0.47, "hz": 0.59, "round_radius": 0.07}"#,
SdfNode::rounded_box(0.31, 0.47, 0.59, 0.07),
),
(
"box_frame",
r#"{"hx": 0.31, "hy": 0.47, "hz": 0.59, "edge": 0.05}"#,
SdfNode::box_frame(Vec3::new(0.31, 0.47, 0.59), 0.05),
),
(
"death_star",
r#"{"ra": 0.5, "rb": 0.3, "d": 0.35}"#,
SdfNode::death_star(0.5, 0.3, 0.35),
),
("egg", r#"{"ra": 0.5, "rb": 0.3}"#, SdfNode::egg(0.5, 0.3)),
(
"gyroid",
r#"{"scale": 2.0, "thickness": 0.1}"#,
SdfNode::gyroid(2.0, 0.1),
),
(
"schwarz_p",
r#"{"scale": 2.0, "thickness": 0.1}"#,
SdfNode::schwarz_p(2.0, 0.1),
),
(
"neovius",
r#"{"scale": 2.0, "thickness": 0.1}"#,
SdfNode::neovius(2.0, 0.1),
),
(
"superellipsoid",
r#"{"width": 0.8, "height": 0.7, "depth": 0.5, "e1": 0.7, "e2": 1.3}"#,
SdfNode::superellipsoid(0.8, 0.7, 0.5, 0.7, 1.3),
),
(
"cut_sphere",
r#"{"radius": 0.6, "cut_height": 0.2}"#,
SdfNode::cut_sphere(0.6, 0.2),
),
(
"solid_angle",
r#"{"angle": 0.9, "radius": 0.6}"#,
SdfNode::solid_angle(0.9, 0.6),
),
(
"vesica",
r#"{"radius": 0.6, "dist": 0.25}"#,
SdfNode::vesica(0.6, 0.25),
),
("heart", r#"{"size": 0.6}"#, SdfNode::heart(0.6)),
(
"infinite_cylinder",
r#"{"radius": 0.6}"#,
SdfNode::infinite_cylinder(0.6),
),
(
"circle_2d",
r#"{"radius": 0.6, "half_height": 0.3}"#,
SdfNode::circle_2d(0.6, 0.3),
),
(
"rect_2d",
r#"{"half_w": 0.4, "half_h": 0.3, "half_height": 0.3}"#,
SdfNode::rect_2d(0.4, 0.3, 0.3),
),
(
"annular_2d",
r#"{"outer_radius": 0.5, "thickness": 0.1, "half_height": 0.3}"#,
SdfNode::annular_2d(0.5, 0.1, 0.3),
),
];
let mut failures = Vec::new();
for (shape, json, want_node) in &cases {
let Some(got_node) = build_sdf_from_params(shape, json) else {
failures.push(format!("{shape}: did not build"));
continue;
};
for p in &pts {
let got = eval(&got_node, *p);
let want = eval(want_node, *p);
if got.to_bits() != want.to_bits() {
failures.push(format!(
"{shape} at {p:?}: {got:e} ({:#010x}) vs constructor {want:e} ({:#010x})",
got.to_bits(),
want.to_bits()
));
break;
}
}
}
assert!(
failures.is_empty(),
"{} shape(s) do not forward to the named constructor:\n{}",
failures.len(),
failures.join("\n")
);
}
#[test]
fn malformed_input_returns_none() {
let params = every_param();
assert!(
build_sdf_from_params("not_a_shape", ¶ms).is_none(),
"unknown shape"
);
assert!(
build_sdf_from_params("", ¶ms).is_none(),
"empty shape name"
);
assert!(
build_sdf_from_params("sphere", "not json").is_none(),
"malformed JSON"
);
assert!(
build_sdf_from_params("sphere", "[]").is_none(),
"JSON array"
);
assert!(
build_sdf_from_params("sphere", "{}").is_none(),
"missing radius"
);
assert!(
build_sdf_from_params("sphere", r#"{"radius": "big"}"#).is_none(),
"radius as a string"
);
assert!(
build_sdf_from_params("sphere", r#"{"radius": null}"#).is_none(),
"radius as null"
);
assert!(
build_sdf_from_params("Sphere", ¶ms).is_none(),
"wrong case"
);
}
}