#![allow(
clippy::disallowed_macros,
reason = "raw-helper: this module calls the platform f32/f64 max/min via crate::raw_minmax::raw_minmax_here!() at sites proven safe by nonzero-const:/both-abs:/int-cast: reasoning, see scripts/raw_minmax_manifest.toml"
)]
crate::raw_minmax::raw_minmax_here!(max_f32);
use std::fmt::Write as _;
use crate::intent::Program;
use crate::SdfNode;
use glam::{Vec2, Vec3};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum EmitError {
Unsupported {
variant: &'static str,
reason: &'static str,
},
NonFinite,
NonUnitRotation {
deviation: f32,
},
}
impl std::fmt::Display for EmitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unsupported { variant, reason } => {
write!(f, "SdfNode::{variant} has no LOL text form: {reason}")
}
Self::NonFinite => {
write!(
f,
"the tree holds a number that is NaN or infinite (LOL numbers are finite)"
)
}
Self::NonUnitRotation { deviation } => {
write!(
f,
"a Rotate node's quaternion is not unit length (|q|^2-1={deviation:e}): \
use rotate(x, y, z) in degrees, or a normalised quaternion"
)
}
}
}
}
impl std::error::Error for EmitError {}
#[must_use]
pub fn fmt_f32(v: f32) -> String {
format!("{v:?}")
}
pub(crate) const FMT_F32_FIDELITY_PROBE_VALUES: &[f32] = &[1.0, -2.0, 0.5, 1.25, 0.1, -0.0, 4e-7];
fn u(v: u32) -> String {
format!("{v}.0")
}
pub fn to_lol(node: &SdfNode) -> Result<String, EmitError> {
let mut out = String::new();
write_node(node, &mut out)?;
Ok(out)
}
fn write_node(node: &SdfNode, out: &mut String) -> Result<(), EmitError> {
stacker::maybe_grow(1024 * 1024, 16 * 1024 * 1024, || {
write_node_inner(node, out)
})
}
#[allow(clippy::too_many_lines)] fn write_node_inner(node: &SdfNode, out: &mut String) -> Result<(), EmitError> {
macro_rules! prim {
($name:expr, $($arg:expr),+) => {{
let args: Vec<String> = vec![$($arg),+];
let _ = write!(out, "{}({})", $name, args.join(", "));
Ok(())
}};
}
macro_rules! modif {
($name:expr, $child:expr $(, $arg:expr)*) => {{
let _ = write!(out, "{}(", $name);
$( let _ = write!(out, "{}, ", $arg); )*
write_node($child, out)?;
out.push(')');
Ok(())
}};
}
macro_rules! binop {
($name:expr, $a:expr, $b:expr $(, $arg:expr)*) => {{
let _ = write!(out, "{}(", $name);
$( let _ = write!(out, "{}, ", $arg); )*
write_node($a, out)?;
out.push_str(", ");
write_node($b, out)?;
out.push(')');
Ok(())
}};
}
let non_finite = std::cell::Cell::new(false);
let f = |v: f32| {
if !v.is_finite() {
non_finite.set(true);
}
fmt_f32(v)
};
let v3 = |v: Vec3| format!("{}, {}, {}", f(v.x), f(v.y), f(v.z));
let v2 = |v: Vec2| format!("{}, {}", f(v.x), f(v.y));
let written = match node {
SdfNode::Sphere { radius } => prim!("sphere", f(*radius)),
SdfNode::Box3d { half_extents } => prim!("box3d", v3(*half_extents)),
SdfNode::RoundedBox {
half_extents,
round_radius,
} => prim!("rounded_box", v3(*half_extents), f(*round_radius)),
SdfNode::Cylinder {
radius,
half_height,
} => prim!("cylinder", f(*radius), f(*half_height)),
SdfNode::Torus {
major_radius,
minor_radius,
} => prim!("torus", f(*major_radius), f(*minor_radius)),
SdfNode::Cone {
radius,
half_height,
} => prim!("cone", f(*radius), f(*half_height)),
SdfNode::Capsule {
point_a,
point_b,
radius,
} => {
let h = point_b.y;
let symmetric = point_a.x == 0.0
&& point_a.z == 0.0
&& point_b.x == 0.0
&& point_b.z == 0.0
&& (point_a.y + h).abs()
<= f32::EPSILON
* raw_max_f32(h.abs(), 1.0, crate::raw_minmax::Reason::NonzeroConst);
if symmetric {
prim!("capsule", f(*radius), f(h))
} else {
prim!("capsule_ab", v3(*point_a), v3(*point_b), f(*radius))
}
}
SdfNode::Ellipsoid { radii } => prim!("ellipsoid", v3(*radii)),
SdfNode::Plane { normal, distance } => prim!("plane", v3(*normal), f(*distance)),
SdfNode::Octahedron { size } => prim!("octahedron", f(*size)),
SdfNode::RoundedCone {
r1,
r2,
half_height,
} => prim!("rounded_cone", f(*r1), f(*r2), f(*half_height)),
SdfNode::Pyramid { half_height } => prim!("pyramid", f(*half_height)),
SdfNode::HexPrism {
hex_radius,
half_height,
} => prim!("hex_prism", f(*hex_radius), f(*half_height)),
SdfNode::Link {
half_length,
r1,
r2,
} => prim!("link", f(*half_length), f(*r1), f(*r2)),
SdfNode::CappedCone {
half_height,
r1,
r2,
} => prim!("capped_cone", f(*half_height), f(*r1), f(*r2)),
SdfNode::CappedTorus {
major_radius,
minor_radius,
cap_angle,
} => prim!(
"capped_torus",
f(*major_radius),
f(*minor_radius),
f(*cap_angle)
),
SdfNode::RoundedCylinder {
radius,
round_radius,
half_height,
} => prim!(
"rounded_cylinder",
f(*radius),
f(*round_radius),
f(*half_height)
),
SdfNode::Tube {
outer_radius,
thickness,
half_height,
} => prim!("tube", f(*outer_radius), f(*thickness), f(*half_height)),
SdfNode::Barrel {
radius,
half_height,
bulge,
} => prim!("barrel", f(*radius), f(*half_height), f(*bulge)),
SdfNode::Heart { size } => prim!("heart", f(*size)),
SdfNode::Egg { ra, rb } => prim!("egg", f(*ra), f(*rb)),
SdfNode::Helix {
major_r,
minor_r,
pitch,
half_height,
} => prim!(
"helix",
f(*major_r),
f(*minor_r),
f(*pitch),
f(*half_height)
),
SdfNode::Tetrahedron { size } => prim!("tetrahedron", f(*size)),
SdfNode::BoxFrame { half_extents, edge } => {
prim!("box_frame", v3(*half_extents), f(*edge))
}
SdfNode::Diamond {
radius,
half_height,
} => prim!("diamond", f(*radius), f(*half_height)),
SdfNode::StarPolygon {
radius,
n_points,
m,
half_height,
} => prim!(
"star_polygon",
f(*radius),
f(*n_points),
f(*m),
f(*half_height)
),
SdfNode::CrossShape {
length,
thickness,
round_radius,
half_height,
} => prim!(
"cross_shape",
f(*length),
f(*thickness),
f(*round_radius),
f(*half_height)
),
SdfNode::Triangle {
point_a,
point_b,
point_c,
} => prim!("triangle", v3(*point_a), v3(*point_b), v3(*point_c)),
SdfNode::Bezier {
point_a,
point_b,
point_c,
radius,
} => prim!(
"bezier",
v3(*point_a),
v3(*point_b),
v3(*point_c),
f(*radius)
),
SdfNode::TriangularPrism { width, half_depth } => {
prim!("triangular_prism", f(*width), f(*half_depth))
}
SdfNode::CutSphere { radius, cut_height } => {
prim!("cut_sphere", f(*radius), f(*cut_height))
}
SdfNode::CutHollowSphere {
radius,
cut_height,
thickness,
} => prim!(
"cut_hollow_sphere",
f(*radius),
f(*cut_height),
f(*thickness)
),
SdfNode::DeathStar { ra, rb, d } => prim!("death_star", f(*ra), f(*rb), f(*d)),
SdfNode::SolidAngle { angle, radius } => prim!("solid_angle", f(*angle), f(*radius)),
SdfNode::Rhombus {
la,
lb,
half_height,
round_radius,
} => prim!("rhombus", f(*la), f(*lb), f(*half_height), f(*round_radius)),
SdfNode::Horseshoe {
angle,
radius,
half_length,
width,
thickness,
} => prim!(
"horseshoe",
f(*angle),
f(*radius),
f(*half_length),
f(*width),
f(*thickness)
),
SdfNode::Vesica { radius, half_dist } => prim!("vesica", f(*radius), f(*half_dist)),
SdfNode::InfiniteCylinder { radius } => prim!("infinite_cylinder", f(*radius)),
SdfNode::InfiniteCone { angle } => prim!("infinite_cone", f(*angle)),
SdfNode::Gyroid { scale, thickness } => prim!("gyroid", f(*scale), f(*thickness)),
SdfNode::ChamferedCube {
half_extents,
chamfer,
} => prim!("chamfered_cube", v3(*half_extents), f(*chamfer)),
SdfNode::SchwarzP { scale, thickness } => prim!("schwarz_p", f(*scale), f(*thickness)),
SdfNode::Superellipsoid {
half_extents,
e1,
e2,
} => prim!("superellipsoid", v3(*half_extents), f(*e1), f(*e2)),
SdfNode::RoundedX {
width,
round_radius,
half_height,
} => prim!("rounded_x", f(*width), f(*round_radius), f(*half_height)),
SdfNode::Pie {
angle,
radius,
half_height,
} => prim!("pie", f(*angle), f(*radius), f(*half_height)),
SdfNode::Trapezoid {
r1,
r2,
trap_height,
half_depth,
} => prim!("trapezoid", f(*r1), f(*r2), f(*trap_height), f(*half_depth)),
SdfNode::Parallelogram {
width,
para_height,
skew,
half_depth,
} => prim!(
"parallelogram",
f(*width),
f(*para_height),
f(*skew),
f(*half_depth)
),
SdfNode::Tunnel {
width,
height_2d,
half_depth,
} => prim!("tunnel", f(*width), f(*height_2d), f(*half_depth)),
SdfNode::UnevenCapsule {
r1,
r2,
cap_height,
half_depth,
} => prim!(
"uneven_capsule",
f(*r1),
f(*r2),
f(*cap_height),
f(*half_depth)
),
SdfNode::ArcShape {
aperture,
radius,
thickness,
half_height,
} => prim!(
"arc_shape",
f(*aperture),
f(*radius),
f(*thickness),
f(*half_height)
),
SdfNode::Moon {
d,
ra,
rb,
half_height,
} => prim!("moon", f(*d), f(*ra), f(*rb), f(*half_height)),
SdfNode::BlobbyCross { size, half_height } => {
prim!("blobby_cross", f(*size), f(*half_height))
}
SdfNode::ParabolaSegment {
width,
para_height,
half_depth,
} => prim!(
"parabola_segment",
f(*width),
f(*para_height),
f(*half_depth)
),
SdfNode::RegularPolygon {
radius,
n_sides,
half_height,
} => prim!("regular_polygon", f(*radius), f(*n_sides), f(*half_height)),
SdfNode::Stairs {
step_width,
step_height,
n_steps,
half_depth,
} => prim!(
"stairs_prim",
f(*step_width),
f(*step_height),
f(*n_steps),
f(*half_depth)
),
SdfNode::Dodecahedron { radius } => prim!("dodecahedron", f(*radius)),
SdfNode::Icosahedron { radius } => prim!("icosahedron", f(*radius)),
SdfNode::TruncatedOctahedron { radius } => prim!("truncated_octahedron", f(*radius)),
SdfNode::TruncatedIcosahedron { radius } => prim!("truncated_icosahedron", f(*radius)),
SdfNode::DiamondSurface { scale, thickness } => {
prim!("diamond_surface", f(*scale), f(*thickness))
}
SdfNode::Neovius { scale, thickness } => prim!("neovius", f(*scale), f(*thickness)),
SdfNode::Lidinoid { scale, thickness } => prim!("lidinoid", f(*scale), f(*thickness)),
SdfNode::IWP { scale, thickness } => prim!("iwp", f(*scale), f(*thickness)),
SdfNode::FRD { scale, thickness } => prim!("frd", f(*scale), f(*thickness)),
SdfNode::FischerKochS { scale, thickness } => {
prim!("fischer_koch_s", f(*scale), f(*thickness))
}
SdfNode::PMY { scale, thickness } => prim!("pmy", f(*scale), f(*thickness)),
SdfNode::Circle2D {
radius,
half_height,
} => prim!("circle_2d", f(*radius), f(*half_height)),
SdfNode::Rect2D {
half_extents,
half_height,
} => prim!("rect_2d", v2(*half_extents), f(*half_height)),
SdfNode::Segment2D {
a,
b,
thickness,
half_height,
} => prim!("segment_2d", v2(*a), v2(*b), f(*thickness), f(*half_height)),
SdfNode::RoundedRect2D {
half_extents,
round_radius,
half_height,
} => prim!(
"rounded_rect_2d",
v2(*half_extents),
f(*round_radius),
f(*half_height)
),
SdfNode::Annular2D {
outer_radius,
thickness,
half_height,
} => prim!(
"annular_2d",
f(*outer_radius),
f(*thickness),
f(*half_height)
),
SdfNode::Terrain { scale, amplitude } => prim!("terrain", f(*scale), f(*amplitude)),
SdfNode::Union { .. }
| SdfNode::Intersection { .. }
| SdfNode::SmoothUnion { .. }
| SdfNode::SmoothIntersection { .. }
| SdfNode::ChamferUnion { .. }
| SdfNode::ChamferIntersection { .. }
| SdfNode::StairsUnion { .. }
| SdfNode::StairsIntersection { .. }
| SdfNode::ColumnsUnion { .. }
| SdfNode::ColumnsIntersection { .. }
| SdfNode::ExpSmoothUnion { .. }
| SdfNode::ExpSmoothIntersection { .. } => write_variadic(node, out),
SdfNode::Subtraction { a, b } => binop!("subtract", a, b),
SdfNode::XOR { a, b } => binop!("xor", a, b),
SdfNode::SmoothSubtraction { a, b, k } => binop!("smooth_subtract", a, b, f(*k)),
SdfNode::ChamferSubtraction { a, b, r } => binop!("chamfer_subtraction", a, b, f(*r)),
SdfNode::StairsSubtraction { a, b, r, n } => {
binop!("stairs_subtraction", a, b, f(*r), f(*n))
}
SdfNode::Pipe { a, b, r } => binop!("pipe", a, b, f(*r)),
SdfNode::Engrave { a, b, r } => binop!("engrave", a, b, f(*r)),
SdfNode::Groove { a, b, ra, rb } => binop!("groove", a, b, f(*ra), f(*rb)),
SdfNode::Tongue { a, b, ra, rb } => binop!("tongue", a, b, f(*ra), f(*rb)),
SdfNode::ColumnsSubtraction { a, b, r, n } => {
binop!("columns_subtraction", a, b, f(*r), f(*n))
}
SdfNode::ExpSmoothSubtraction { a, b, k } => {
binop!("exp_smooth_subtraction", a, b, f(*k))
}
SdfNode::Morph { a, b, t } => binop!("morph", a, b, f(*t)),
SdfNode::Translate { child, offset } => modif!("translate", child, v3(*offset)),
SdfNode::Rotate { child, rotation } => {
if let Err(crate::limits::SpecError::FloatLimit(fe)) =
crate::limits::checked_rotation_quat(rotation.x, rotation.y, rotation.z, rotation.w)
{
return Err(EmitError::NonUnitRotation {
deviation: fe.value,
});
}
modif!(
"rotate_q",
child,
f(rotation.x),
f(rotation.y),
f(rotation.z),
f(rotation.w)
)
}
SdfNode::Scale { child, factor } => modif!("scale", child, f(*factor)),
SdfNode::ScaleNonUniform { child, factors } => {
modif!("scale_non_uniform", child, v3(*factors))
}
SdfNode::Round { child, radius } => modif!("round", child, f(*radius)),
SdfNode::Onion { child, thickness } => modif!("onion", child, f(*thickness)),
SdfNode::Twist { child, strength } => modif!("twist", child, f(*strength)),
SdfNode::Bend { child, curvature } => modif!("bend", child, f(*curvature)),
SdfNode::Mirror { child, axes } => modif!("mirror", child, v3(*axes)),
SdfNode::RepeatInfinite { child, spacing } => modif!("repeat", child, v3(*spacing)),
SdfNode::Elongate { child, amount } => modif!("elongate", child, v3(*amount)),
SdfNode::Revolution { child, offset } => modif!("revolution", child, f(*offset)),
SdfNode::Extrude { child, half_height } => modif!("extrude", child, f(*half_height)),
SdfNode::Taper { child, factor, .. } => modif!("taper", child, f(*factor)),
SdfNode::Displacement { child, strength } => modif!("displacement", child, f(*strength)),
SdfNode::PolarRepeat { child, count } => modif!("polar_repeat", child, u(*count)),
SdfNode::Shear { child, shear } => modif!("shear", child, v3(*shear)),
SdfNode::Noise {
child,
amplitude,
frequency,
seed,
} => modif!("noise", child, f(*amplitude), f(*frequency), u(*seed)),
SdfNode::RepeatFinite {
child,
count,
spacing,
} => modif!(
"repeat_finite",
child,
u(count[0]),
u(count[1]),
u(count[2]),
v3(*spacing)
),
SdfNode::OctantMirror { child } => modif!("octant_mirror", child),
SdfNode::IcosahedralSymmetry { child } => modif!("icosahedral_symmetry", child),
SdfNode::WithMaterial { child, material_id } => {
modif!("with_material", child, u(*material_id))
}
SdfNode::SurfaceRoughness {
child,
frequency,
amplitude,
octaves,
} => modif!(
"surface_roughness",
child,
f(*frequency),
f(*amplitude),
u(*octaves)
),
SdfNode::SweepBezier { child, p0, p1, p2 } => {
modif!("sweep_bezier", child, v2(*p0), v2(*p1), v2(*p2))
}
SdfNode::Animated {
child,
speed,
amplitude,
} => modif!("animate", child, f(*speed), f(*amplitude)),
SdfNode::IFS { .. } => Err(EmitError::Unsupported {
variant: "IFS",
reason: "iterated function systems have no LOL construct",
}),
SdfNode::SdfSkinning { .. } => Err(EmitError::Unsupported {
variant: "SdfSkinning",
reason: "bone skinning has no LOL construct",
}),
SdfNode::LatticeDeform { .. } => Err(EmitError::Unsupported {
variant: "LatticeDeform",
reason: "lattice deformation has no LOL construct",
}),
SdfNode::ProjectiveTransform { .. } => Err(EmitError::Unsupported {
variant: "ProjectiveTransform",
reason: "4x4 projective transform has no LOL construct",
}),
SdfNode::HeightmapDisplacement { .. } => Err(EmitError::Unsupported {
variant: "HeightmapDisplacement",
reason: "heightmap data (Phase 1) has no LOL construct",
}),
SdfNode::SineDisplacement { .. } => Err(EmitError::Unsupported {
variant: "SineDisplacement",
reason: "sine displacement has no LOL construct",
}),
SdfNode::Polygon2D { .. } => Err(EmitError::Unsupported {
variant: "Polygon2D",
reason: "arbitrary vertex lists have no LOL construct",
}),
SdfNode::MetricBall { .. } => Err(EmitError::Unsupported {
variant: "MetricBall",
reason: "weighted-norm balls have no LOL construct (alice-sdf 4.0)",
}),
SdfNode::MetricBlend { .. } => Err(EmitError::Unsupported {
variant: "MetricBlend",
reason: "metric blends have no LOL construct (alice-sdf 4.0)",
}),
_ => Err(EmitError::Unsupported {
variant: "<unknown>",
reason: "variant added in a newer alice-sdf than this emitter knows",
}),
};
written?;
if non_finite.get() {
return Err(EmitError::NonFinite);
}
Ok(())
}
fn variadic_parts(node: &SdfNode) -> Option<(&'static str, Vec<f32>, &SdfNode, &SdfNode)> {
Some(match node {
SdfNode::Union { a, b } => ("union", vec![], a, b),
SdfNode::Intersection { a, b } => ("intersection", vec![], a, b),
SdfNode::SmoothUnion { a, b, k } => ("smooth_union", vec![*k], a, b),
SdfNode::SmoothIntersection { a, b, k } => ("smooth_intersection", vec![*k], a, b),
SdfNode::ChamferUnion { a, b, r } => ("chamfer_union", vec![*r], a, b),
SdfNode::ChamferIntersection { a, b, r } => ("chamfer_intersection", vec![*r], a, b),
SdfNode::StairsUnion { a, b, r, n } => ("stairs_union", vec![*r, *n], a, b),
SdfNode::StairsIntersection { a, b, r, n } => ("stairs_intersection", vec![*r, *n], a, b),
SdfNode::ColumnsUnion { a, b, r, n } => ("columns_union", vec![*r, *n], a, b),
SdfNode::ColumnsIntersection { a, b, r, n } => ("columns_intersection", vec![*r, *n], a, b),
SdfNode::ExpSmoothUnion { a, b, k } => ("exp_smooth_union", vec![*k], a, b),
SdfNode::ExpSmoothIntersection { a, b, k } => ("exp_smooth_intersection", vec![*k], a, b),
_ => return None,
})
}
fn write_variadic(node: &SdfNode, out: &mut String) -> Result<(), EmitError> {
let (name, args, mut a, b) =
variadic_parts(node).expect("write_variadic called on a non-variadic node");
let mut rev_children: Vec<&SdfNode> = vec![b];
while let Some((n2, args2, a2, b2)) = variadic_parts(a) {
let same = args2.len() == args.len()
&& args2
.iter()
.zip(&args)
.all(|(x, y)| x.to_bits() == y.to_bits());
if n2 != name || !same {
break;
}
rev_children.push(b2);
a = a2;
}
rev_children.push(a);
if args.iter().any(|v| !v.is_finite()) {
return Err(EmitError::NonFinite);
}
let _ = write!(out, "{name}(");
for v in &args {
let _ = write!(out, "{}, ", fmt_f32(*v));
}
for (i, c) in rev_children.iter().rev().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_node(c, out)?;
}
out.push(')');
Ok(())
}
impl Program {
pub fn to_lol(&self) -> Result<String, EmitError> {
if self.sdf_registry.is_empty() && self.intent.is_none() {
return to_lol(&self.sdf);
}
let mut out = String::from("program(");
write_node(&self.sdf, &mut out)?;
out.push_str(", entities(");
for (i, e) in self.sdf_registry.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_node(e, &mut out)?;
}
out.push(')');
if let Some(intent) = &self.intent {
out.push_str(", ");
out.push_str(&intent.to_lol());
}
out.push(')');
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime_parser::{parse_lol, parse_program};
#[test]
fn fmt_f32_forms() {
assert_eq!(fmt_f32(1.0), "1.0");
assert_eq!(fmt_f32(-2.0), "-2.0");
assert_eq!(fmt_f32(0.5), "0.5");
assert_eq!(fmt_f32(1.25), "1.25");
assert_eq!(fmt_f32(0.1), "0.1");
assert_eq!(fmt_f32(-0.0), "-0.0");
assert_eq!(fmt_f32(4e-7), "4e-7");
}
#[test]
fn primitives_and_csg_round_trip_textually() {
for src in [
"sphere(1.0)",
"box3d(1.0, 2.0, 3.0)",
"union(sphere(1.0), box3d(0.5, 0.5, 0.5), cylinder(0.3, 2.0))",
"smooth_union(0.3, sphere(1.0), box3d(0.5, 0.5, 0.5))",
"subtract(cylinder(2.5, 0.125), polar_repeat(12.0, translate(1.8, 0.0, 0.0, cylinder(0.3, 0.2))))",
"repeat_finite(3.0, 2.0, 1.0, 10.0, 10.0, 0.0, sphere(1.0))",
"translate(0.0, 1.5, 0.0, scale(2.0, sphere(0.5)))",
] {
let node = parse_lol(src).unwrap();
assert_eq!(to_lol(&node).unwrap(), src);
}
}
#[test]
fn variadic_ops_are_flattened_back() {
let node = parse_lol("union(sphere(1.0), sphere(2.0), sphere(3.0))").unwrap();
assert_eq!(
to_lol(&node).unwrap(),
"union(sphere(1.0), sphere(2.0), sphere(3.0))"
);
let nested = parse_lol("union(union(sphere(1.0), sphere(2.0)), sphere(3.0))").unwrap();
assert_eq!(
to_lol(&nested).unwrap(),
"union(sphere(1.0), sphere(2.0), sphere(3.0))"
);
let right = parse_lol("union(sphere(1.0), union(sphere(2.0), sphere(3.0)))").unwrap();
assert_eq!(
to_lol(&right).unwrap(),
"union(sphere(1.0), union(sphere(2.0), sphere(3.0)))"
);
let mixed = parse_lol(
"smooth_union(0.5, smooth_union(0.2, sphere(1.0), sphere(2.0)), sphere(3.0))",
)
.unwrap();
assert_eq!(
to_lol(&mixed).unwrap(),
"smooth_union(0.5, smooth_union(0.2, sphere(1.0), sphere(2.0)), sphere(3.0))"
);
}
#[test]
fn variadic_merge_does_not_fold_values_closer_than_the_old_tolerance() {
let text =
"smooth_union(0.5, smooth_union(0.5000001, sphere(1.0), sphere(2.0)), sphere(3.0))";
let node = parse_lol(text).unwrap();
assert_eq!(
to_lol(&node).unwrap(),
text,
"two different (if close) k values must not merge into one smooth_union call"
);
}
#[test]
fn variadic_merge_does_not_fold_negative_and_positive_zero() {
let text = "smooth_union(0.0, smooth_union(-0.0, sphere(1.0), sphere(2.0)), sphere(3.0))";
let node = parse_lol(text).unwrap();
assert_eq!(
to_lol(&node).unwrap(),
text,
"0.0 and -0.0 k values must not merge into one smooth_union call"
);
}
#[test]
fn rotate_with_euler_degrees_emits_as_rotate_q_and_round_trips_bit_exact() {
let node = parse_lol("rotate(90.0, 0.0, 0.0, cylinder(1.0, 2.0))").unwrap();
let text = to_lol(&node).unwrap();
assert!(text.starts_with("rotate_q("), "{text}");
let back = parse_lol(&text).unwrap();
let text2 = to_lol(&back).unwrap();
assert_eq!(
text, text2,
"rotate_q must round-trip bit-exact, not just eval-agree"
);
for p in [
Vec3::new(0.0, 0.0, 1.5),
Vec3::new(0.0, 1.5, 0.0),
Vec3::ZERO,
] {
assert!((crate::eval(&node, p) - crate::eval(&back, p)).abs() < 1e-4);
}
}
#[test]
fn to_lol_refuses_a_code_built_non_unit_rotation() {
let node = SdfNode::Rotate {
child: std::sync::Arc::new(SdfNode::Sphere { radius: 1.0 }),
rotation: glam::Quat::from_xyzw(0.0, 0.0, 0.0, 2.0),
};
let err = to_lol(&node).unwrap_err();
assert!(
matches!(err, EmitError::NonUnitRotation { .. }),
"expected NonUnitRotation, got {err:?}"
);
}
#[test]
fn to_lol_accepts_a_code_built_unit_rotation() {
let node = SdfNode::Rotate {
child: std::sync::Arc::new(SdfNode::Sphere { radius: 1.0 }),
rotation: glam::Quat::from_xyzw(0.0, 0.0, 0.0, 1.0),
};
let text = to_lol(&node).unwrap();
assert_eq!(text, "rotate_q(0.0, 0.0, 0.0, 1.0, sphere(1.0))");
assert!(parse_lol(&text).is_ok());
}
#[test]
fn program_round_trip() {
let src = "program(sphere(1.0), entities(box3d(0.1, 0.1, 0.1)), seq(grasp(0, right, 5.0), rest(500)))";
let p = parse_program(src).unwrap();
assert_eq!(p.to_lol().unwrap(), src);
let bare = parse_program("sphere(2.0)").unwrap();
assert_eq!(bare.to_lol().unwrap(), "sphere(2.0)");
}
#[test]
fn capsule_uses_short_form_only_when_y_symmetric() {
let node = SdfNode::Capsule {
point_a: Vec3::new(1.0, 0.0, 0.0),
point_b: Vec3::new(0.0, 1.0, 0.0),
radius: 0.5,
};
assert_eq!(
to_lol(&node).unwrap(),
"capsule_ab(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.5)"
);
let sym = parse_lol("capsule(0.5, 2.0)").unwrap();
assert_eq!(to_lol(&sym).unwrap(), "capsule(0.5, 2.0)");
let back = parse_lol(&to_lol(&node).unwrap()).unwrap();
assert!(matches!(back, SdfNode::Capsule { radius, .. } if (radius - 0.5).abs() < 1e-6));
}
#[test]
fn capsule_tiny_nonzero_on_any_symmetry_coordinate_is_not_short_formed() {
let tiny = 1e-7_f32;
let cases: [(&str, Vec3, Vec3); 4] = [
(
"point_a.x",
Vec3::new(tiny, -1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
),
(
"point_a.z",
Vec3::new(0.0, -1.0, tiny),
Vec3::new(0.0, 1.0, 0.0),
),
(
"point_b.x",
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(tiny, 1.0, 0.0),
),
(
"point_b.z",
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, 1.0, tiny),
),
];
for (name, point_a, point_b) in cases {
let node = SdfNode::Capsule {
point_a,
point_b,
radius: 0.5,
};
let text = to_lol(&node).unwrap();
assert!(
text.starts_with("capsule_ab("),
"{name}: a 1e-7 coordinate must not be treated as symmetric: {text:?}"
);
let back = parse_lol(&text).unwrap();
let SdfNode::Capsule {
point_a: pa,
point_b: pb,
..
} = back
else {
panic!("expected Capsule, got {back:?}");
};
let got = match name {
"point_a.x" => pa.x,
"point_a.z" => pa.z,
"point_b.x" => pb.x,
"point_b.z" => pb.z,
_ => unreachable!(),
};
assert_eq!(got.to_bits(), tiny.to_bits(), "{name}: {text:?}");
}
}
#[test]
fn ifs_is_unsupported() {
let node = SdfNode::IFS {
child: std::sync::Arc::new(SdfNode::Sphere { radius: 1.0 }),
transforms: Vec::new(),
iterations: 1,
};
assert!(matches!(
to_lol(&node),
Err(EmitError::Unsupported { variant: "IFS", .. })
));
}
}