use super::aabb::{primitives as aabb_prims, AabbPacked};
use super::eval_bvh::CompiledSdfBvh;
use super::instruction::Instruction;
use super::opcode::{OpCode, OpKind};
use glam::{Quat, Vec2, Vec3};
use std::collections::HashSet;
#[derive(Debug, thiserror::Error)]
pub enum RefitError {
#[error("opcode {opcode:?} at instruction {instruction_index} is not supported by refit; fall back to SdfNode-based refit_bvh")]
UnsupportedOpcode {
opcode: OpCode,
instruction_index: usize,
},
#[error(
"value stack underflow at instruction {instruction_index} ({opcode:?}); malformed bytecode"
)]
ValueStackUnderflow {
instruction_index: usize,
opcode: OpCode,
},
#[error("transform stack underflow at PopTransform (instruction {instruction_index})")]
TransformStackUnderflow {
instruction_index: usize,
},
#[error("dirty index {index} out of bounds ({len} instructions)")]
DirtyIndexOutOfBounds {
index: usize,
len: usize,
},
#[error("bvh internal invariant: aabbs.len()={aabbs} != instructions.len()={instructions}")]
LengthMismatch {
aabbs: usize,
instructions: usize,
},
}
pub fn build_parent_indices(bvh: &CompiledSdfBvh) -> Result<Vec<Option<u32>>, RefitError> {
let n = bvh.instructions.len();
let mut parent = vec![None; n];
let mut value_stack: Vec<usize> = Vec::new();
let mut transform_stack: Vec<usize> = Vec::new();
for (i, inst) in bvh.instructions.iter().enumerate() {
let opcode = inst.opcode;
match opcode.kind() {
OpKind::Primitive => {
if let Some(&t) = transform_stack.last() {
parent[i] = Some(t as u32);
}
value_stack.push(i);
}
OpKind::Binary => {
let b = value_stack.pop().ok_or(RefitError::ValueStackUnderflow {
instruction_index: i,
opcode,
})?;
let a = value_stack.pop().ok_or(RefitError::ValueStackUnderflow {
instruction_index: i,
opcode,
})?;
parent[a] = Some(i as u32);
parent[b] = Some(i as u32);
if let Some(&t) = transform_stack.last() {
parent[i] = Some(t as u32);
}
value_stack.push(i);
}
OpKind::Transform | OpKind::Modifier => {
if let Some(&t) = transform_stack.last() {
parent[i] = Some(t as u32);
}
transform_stack.push(i);
}
OpKind::PopTransform => {
let t = transform_stack
.pop()
.ok_or(RefitError::TransformStackUnderflow {
instruction_index: i,
})?;
let child = value_stack.pop().ok_or(RefitError::ValueStackUnderflow {
instruction_index: i,
opcode,
})?;
parent[child] = Some(t as u32);
value_stack.push(t);
}
OpKind::End => break,
}
}
Ok(parent)
}
pub fn build_subtree_ends(bvh: &CompiledSdfBvh) -> Result<Vec<u32>, RefitError> {
let n = bvh.instructions.len();
let mut ends: Vec<u32> = (0..n as u32).collect(); let mut transform_stack: Vec<usize> = Vec::new();
for (i, inst) in bvh.instructions.iter().enumerate() {
let opcode = inst.opcode;
match opcode.kind() {
OpKind::Transform | OpKind::Modifier => {
transform_stack.push(i);
}
OpKind::PopTransform => {
let t = transform_stack
.pop()
.ok_or(RefitError::TransformStackUnderflow {
instruction_index: i,
})?;
ends[t] = i as u32;
}
OpKind::End => break,
OpKind::Primitive | OpKind::Binary => {
}
}
}
Ok(ends)
}
pub fn refit_all(bvh: &mut CompiledSdfBvh) -> Result<usize, RefitError> {
if bvh.aabbs.len() != bvh.instructions.len() {
return Err(RefitError::LengthMismatch {
aabbs: bvh.aabbs.len(),
instructions: bvh.instructions.len(),
});
}
let dirty = None;
walk_and_recompute(bvh, dirty.as_ref())
}
pub fn refit_partial(bvh: &mut CompiledSdfBvh, dirty: &[usize]) -> Result<usize, RefitError> {
if bvh.aabbs.len() != bvh.instructions.len() {
return Err(RefitError::LengthMismatch {
aabbs: bvh.aabbs.len(),
instructions: bvh.instructions.len(),
});
}
let n = bvh.instructions.len();
for &d in dirty {
if d >= n {
return Err(RefitError::DirtyIndexOutOfBounds { index: d, len: n });
}
}
let mut affected: HashSet<usize> = HashSet::with_capacity(dirty.len() * 2);
for &d in dirty {
let mut cursor = Some(d);
while let Some(idx) = cursor {
if !affected.insert(idx) {
break;
}
cursor = bvh
.parent_indices
.get(idx)
.and_then(|p| p.map(|q| q as usize));
}
}
walk_and_recompute(bvh, Some(&affected))
}
fn walk_and_recompute(
bvh: &mut CompiledSdfBvh,
affected: Option<&HashSet<usize>>,
) -> Result<usize, RefitError> {
let n = bvh.instructions.len();
let mut value_stack: Vec<AabbPacked> = Vec::with_capacity(64);
let mut transform_stack: Vec<usize> = Vec::with_capacity(32);
let mut recomputed = 0usize;
let mut scene_aabb: Option<AabbPacked> = None;
let should_recompute = |i: usize| affected.is_none_or(|set| set.contains(&i));
let has_subtree_ends =
bvh.subtree_end.len() == n && affected.is_some() && !bvh.subtree_end.is_empty();
let mut i = 0usize;
while i < n {
let inst = bvh.instructions[i];
let opcode = inst.opcode;
if has_subtree_ends
&& matches!(opcode.kind(), OpKind::Transform | OpKind::Modifier)
&& !should_recompute(i)
{
let end = bvh.subtree_end[i] as usize;
if end > i {
let aabb = bvh.aabbs[i];
value_stack.push(aabb);
scene_aabb = Some(match scene_aabb {
None => aabb,
Some(prev) => prev.union(&aabb),
});
i = end + 1;
continue;
}
}
match opcode.kind() {
OpKind::Primitive => {
let aabb = if should_recompute(i) {
let new_aabb = primitive_aabb(opcode, &inst, &bvh.aux_data);
bvh.aabbs[i] = new_aabb;
recomputed += 1;
new_aabb
} else {
bvh.aabbs[i]
};
value_stack.push(aabb);
scene_aabb = Some(match scene_aabb {
None => aabb,
Some(prev) => prev.union(&aabb),
});
}
OpKind::Binary => {
let b = value_stack.pop().ok_or(RefitError::ValueStackUnderflow {
instruction_index: i,
opcode,
})?;
let a = value_stack.pop().ok_or(RefitError::ValueStackUnderflow {
instruction_index: i,
opcode,
})?;
let aabb = if should_recompute(i) {
let new_aabb = csg_binary_aabb(opcode, &inst.params, a, b);
bvh.aabbs[i] = new_aabb;
recomputed += 1;
new_aabb
} else {
bvh.aabbs[i]
};
value_stack.push(aabb);
scene_aabb = Some(match scene_aabb {
None => aabb,
Some(prev) => prev.union(&aabb),
});
}
OpKind::Transform | OpKind::Modifier => {
transform_stack.push(i);
}
OpKind::PopTransform => {
let t = transform_stack
.pop()
.ok_or(RefitError::TransformStackUnderflow {
instruction_index: i,
})?;
let child = value_stack.pop().ok_or(RefitError::ValueStackUnderflow {
instruction_index: i,
opcode,
})?;
let t_inst = bvh.instructions[t];
let aabb = if should_recompute(t) {
let new_aabb = transform_or_modifier_aabb(t_inst.opcode, &t_inst.params, child);
bvh.aabbs[t] = new_aabb;
recomputed += 1;
new_aabb
} else {
bvh.aabbs[t]
};
value_stack.push(aabb);
scene_aabb = Some(match scene_aabb {
None => aabb,
Some(prev) => prev.union(&aabb),
});
}
OpKind::End => break,
}
i += 1;
}
if let Some(root) = value_stack.last() {
bvh.scene_aabb = *root;
} else if let Some(aabb) = scene_aabb {
bvh.scene_aabb = aabb;
}
Ok(recomputed)
}
fn primitive_aabb(opcode: OpCode, inst: &Instruction, aux_data: &[f32]) -> AabbPacked {
let params = &inst.params;
let cube = |r: f32| AabbPacked::new(Vec3::splat(-r.abs()), Vec3::splat(r.abs()));
match opcode {
OpCode::Sphere => aabb_prims::sphere_aabb(params[0]),
OpCode::Box3d => aabb_prims::box_aabb(Vec3::new(params[0], params[1], params[2])),
OpCode::Cylinder => aabb_prims::cylinder_aabb(params[0], params[1]),
OpCode::Torus => aabb_prims::torus_aabb(params[0], params[1]),
OpCode::Plane => aabb_prims::plane_aabb(),
OpCode::Capsule => {
let a = Vec3::new(params[0], params[1], params[2]);
let b = Vec3::new(params[3], params[4], params[5]);
aabb_prims::capsule_aabb(a, b, params[6])
}
OpCode::Cone => {
let r = params[0];
let hh = params[1];
AabbPacked::new(Vec3::new(-r, -hh, -r), Vec3::new(r, hh, r))
}
OpCode::Ellipsoid => {
let radii = Vec3::new(params[0], params[1], params[2]);
AabbPacked::new(-radii, radii)
}
OpCode::RoundedCone => {
let (r1, r2, hh) = (params[0], params[1], params[2]);
let max_r = r1.max(r2);
AabbPacked::new(
Vec3::new(-max_r, -hh - r1, -max_r),
Vec3::new(max_r, hh + r2, max_r),
)
}
OpCode::Pyramid => {
let hh = params[0];
AabbPacked::new(Vec3::new(-0.5, -hh, -0.5), Vec3::new(0.5, hh, 0.5))
}
OpCode::Octahedron => cube(params[0]),
OpCode::HexPrism => {
let hex_r = params[0];
let hh = params[1];
AabbPacked::new(Vec3::new(-hex_r, -hex_r, -hh), Vec3::new(hex_r, hex_r, hh))
}
OpCode::Link => {
let half_length = params[0];
let r1 = params[1];
let r2 = params[2];
let extent = r1 + r2;
AabbPacked::new(
Vec3::new(-extent, -(half_length + extent), -r2),
Vec3::new(extent, half_length + extent, r2),
)
}
OpCode::RoundedBox | OpCode::ChamferedCube | OpCode::Superellipsoid => {
let he = Vec3::new(params[0], params[1], params[2]);
let r = if opcode == OpCode::RoundedBox {
params[3]
} else {
0.0
};
AabbPacked::new(-(he + r), he + r)
}
OpCode::BoxFrame => {
let he = Vec3::new(params[0], params[1], params[2]) + params[3];
AabbPacked::new(-he, he)
}
OpCode::CappedCone => cube(params[0].max(params[1]).max(params[2])),
OpCode::CappedTorus => cube(params[0] + params[1]),
OpCode::RoundedCylinder => cube((2.0 * params[0]).max(params[2] + params[1])),
OpCode::TriangularPrism => cube(params[0] + params[1]),
OpCode::CutSphere | OpCode::CutHollowSphere => cube(params[0]),
OpCode::DeathStar => cube(params[0] + params[1] + params[2]),
OpCode::SolidAngle => cube(params[1]),
OpCode::Rhombus => cube(params[0] + params[1] + params[2] + params[3]),
OpCode::Horseshoe => cube(params[1] + params[2] + params[3] + params[4]),
OpCode::Vesica => cube(params[0] + params[1]),
OpCode::Heart => cube(params[0] * 2.0),
OpCode::Tube => cube(params[0].max(params[2])),
OpCode::Barrel => cube((params[0] + params[2]).max(params[1])),
OpCode::Diamond => cube(params[0].max(params[1])),
OpCode::RoundedX => cube(params[0] + params[1] + params[2]),
OpCode::Pie => cube(params[1].max(params[2])),
OpCode::Trapezoid => cube(params[0].max(params[1]) + params[2] + params[3]),
OpCode::Parallelogram => cube(params[0] + params[1] + params[2].abs() + params[3]),
OpCode::Tunnel => cube(params[0] + params[1] + params[2]),
OpCode::UnevenCapsule => cube(params[0].max(params[1]) + params[2] + params[3]),
OpCode::Egg => cube((params[0] + params[1]) * 2.0),
OpCode::ArcShape => cube(params[1] + params[2] + params[3]),
OpCode::Moon => cube(params[0].abs() + params[1] + params[2] + params[3]),
OpCode::CrossShape => cube(params[0] + params[1] + params[2] + params[3]),
OpCode::BlobbyCross => cube(params[0] * 2.0 + params[1]),
OpCode::ParabolaSegment => cube(params[0] + params[1] + params[2]),
OpCode::RegularPolygon => cube(params[0] + params[2]),
OpCode::StarPolygon => cube(params[0].max(params[2]) + params[3]),
OpCode::Stairs => cube((params[0] + params[1]) * params[2].max(1.0) + params[3]),
OpCode::Helix => cube(params[0] + params[1] + params[2].abs() + params[3]),
OpCode::Tetrahedron => cube(params[0] * 2.0),
OpCode::Dodecahedron
| OpCode::Icosahedron
| OpCode::TruncatedOctahedron
| OpCode::TruncatedIcosahedron => cube(params[0] * 2.0),
OpCode::InfiniteCylinder
| OpCode::InfiniteCone
| OpCode::Gyroid
| OpCode::SchwarzP
| OpCode::DiamondSurface
| OpCode::Neovius
| OpCode::Lidinoid
| OpCode::IWP
| OpCode::FRD
| OpCode::FischerKochS
| OpCode::PMY => AabbPacked::infinite(),
OpCode::Circle2D => {
let r = params[0];
AabbPacked::new(Vec3::new(-r, -r, -params[1]), Vec3::new(r, r, params[1]))
}
OpCode::Rect2D => {
let he = Vec3::new(params[0], params[1], params[2]);
AabbPacked::new(-he, he)
}
OpCode::RoundedRect2D => {
let he = Vec3::new(params[0], params[1], params[3]);
AabbPacked::new(-he, he)
}
OpCode::Segment2D => {
let r = params[0]
.abs()
.max(params[1].abs())
.max(params[2].abs())
.max(params[3].abs())
+ params[4];
AabbPacked::new(Vec3::new(-r, -r, -params[5]), Vec3::new(r, r, params[5]))
}
OpCode::Annular2D => {
let r = params[0] + params[1];
AabbPacked::new(Vec3::new(-r, -r, -params[2]), Vec3::new(r, r, params[2]))
}
OpCode::Polygon2D => {
let off = inst.aux_offset as usize;
let flat = aux_data
.get(off..off + inst.aux_len as usize)
.unwrap_or(&[]);
let mut mn = Vec2::splat(f32::INFINITY);
let mut mx = Vec2::splat(f32::NEG_INFINITY);
for v in flat.chunks_exact(2) {
mn = mn.min(Vec2::new(v[0], v[1]));
mx = mx.max(Vec2::new(v[0], v[1]));
}
if flat.len() < 6 {
return AabbPacked::empty();
}
AabbPacked::new(
Vec3::new(mn.x, mn.y, -params[0]),
Vec3::new(mx.x, mx.y, params[0]),
)
}
OpCode::Union
| OpCode::Intersection
| OpCode::Subtraction
| OpCode::SmoothUnion
| OpCode::SmoothIntersection
| OpCode::SmoothSubtraction
| OpCode::ChamferUnion
| OpCode::ChamferIntersection
| OpCode::ChamferSubtraction
| OpCode::StairsUnion
| OpCode::StairsIntersection
| OpCode::StairsSubtraction
| OpCode::XOR
| OpCode::Morph
| OpCode::ColumnsUnion
| OpCode::ColumnsIntersection
| OpCode::ColumnsSubtraction
| OpCode::Pipe
| OpCode::Engrave
| OpCode::Groove
| OpCode::Tongue
| OpCode::ExpSmoothUnion
| OpCode::ExpSmoothIntersection
| OpCode::ExpSmoothSubtraction
| OpCode::Translate
| OpCode::Rotate
| OpCode::Scale
| OpCode::ScaleNonUniform
| OpCode::ProjectiveTransform
| OpCode::LatticeDeform
| OpCode::SdfSkinning
| OpCode::Twist
| OpCode::Bend
| OpCode::RepeatInfinite
| OpCode::RepeatFinite
| OpCode::Round
| OpCode::Onion
| OpCode::Elongate
| OpCode::Noise
| OpCode::Mirror
| OpCode::Revolution
| OpCode::Extrude
| OpCode::Taper
| OpCode::Displacement
| OpCode::PolarRepeat
| OpCode::SweepBezier
| OpCode::OctantMirror
| OpCode::Shear
| OpCode::Animated
| OpCode::IcosahedralSymmetry
| OpCode::IFS
| OpCode::HeightmapDisplacement
| OpCode::SurfaceRoughness
| OpCode::PopTransform
| OpCode::End => unreachable!("primitive_aabb called with non-primitive {opcode:?}"),
}
}
fn csg_binary_aabb(opcode: OpCode, params: &[f32; 7], a: AabbPacked, b: AabbPacked) -> AabbPacked {
match opcode {
OpCode::Union | OpCode::XOR | OpCode::Morph | OpCode::Pipe => a.union(&b),
OpCode::SmoothUnion => a.union(&b).expand(params[0]),
OpCode::ChamferUnion => a.union(&b).expand(params[0]),
OpCode::StairsUnion => a.union(&b).expand(params[0]),
OpCode::ColumnsUnion => a.union(&b).expand(params[0]),
OpCode::ExpSmoothUnion => a.union(&b).expand(params[0]),
OpCode::Intersection
| OpCode::SmoothIntersection
| OpCode::ChamferIntersection
| OpCode::StairsIntersection
| OpCode::ColumnsIntersection
| OpCode::ExpSmoothIntersection => a.intersection(&b),
OpCode::Subtraction
| OpCode::SmoothSubtraction
| OpCode::ChamferSubtraction
| OpCode::StairsSubtraction
| OpCode::ColumnsSubtraction
| OpCode::ExpSmoothSubtraction
| OpCode::Engrave
| OpCode::Groove
| OpCode::Tongue => a,
_ => unreachable!("csg_binary_aabb called with non-binary {opcode:?}"),
}
}
fn transform_or_modifier_aabb(opcode: OpCode, params: &[f32; 7], child: AabbPacked) -> AabbPacked {
match opcode {
OpCode::Translate => {
let offset = Vec3::new(params[0], params[1], params[2]);
child.translate(offset)
}
OpCode::Rotate => {
let quat = Quat::from_xyzw(params[0], params[1], params[2], params[3]);
child.rotate(quat)
}
OpCode::Scale => {
child.scale(params[1])
}
OpCode::ScaleNonUniform => {
let sx = 1.0 / params[0];
let sy = 1.0 / params[1];
let sz = 1.0 / params[2];
child.scale_nonuniform(Vec3::new(sx, sy, sz))
}
OpCode::Round => child.expand(params[0]),
OpCode::Onion => child.expand(params[0]),
OpCode::Displacement => child.expand(params[0].abs()),
OpCode::Noise => child.expand(params[0]),
OpCode::Twist => {
let max_extent = child.half_size().max_element();
child.expand(max_extent * params[0].abs() * 0.5)
}
OpCode::Bend => {
let max_extent = child.half_size().max_element();
child.expand(max_extent * params[0].abs())
}
OpCode::Taper => {
let max_extent = child.half_size().max_element();
child.expand(max_extent * params[0].abs())
}
OpCode::Shear => {
let max_shear = params[0].abs().max(params[1].abs()).max(params[2].abs());
let half = child.half_size();
let expand = half.length() * max_shear;
child.expand(expand)
}
OpCode::Elongate => {
let amount = Vec3::new(params[0], params[1], params[2]);
AabbPacked::new(child.min() - amount, child.max() + amount)
}
OpCode::RepeatInfinite => AabbPacked::infinite(),
OpCode::RepeatFinite => {
let cx = params[0];
let cy = params[1];
let cz = params[2];
let sx = params[3];
let sy = params[4];
let sz = params[5];
let expand = Vec3::new(cx * sx, cy * sy, cz * sz);
AabbPacked::new(child.min() - expand, child.max() + expand)
}
OpCode::Mirror => {
let ax = params[0];
let ay = params[1];
let az = params[2];
let cmin = child.min();
let cmax = child.max();
let ex = cmax.x.abs().max(cmin.x.abs());
let ey = cmax.y.abs().max(cmin.y.abs());
let ez = cmax.z.abs().max(cmin.z.abs());
AabbPacked::new(
Vec3::new(
if ax != 0.0 { -ex } else { cmin.x },
if ay != 0.0 { -ey } else { cmin.y },
if az != 0.0 { -ez } else { cmin.z },
),
Vec3::new(
if ax != 0.0 { ex } else { cmax.x },
if ay != 0.0 { ey } else { cmax.y },
if az != 0.0 { ez } else { cmax.z },
),
)
}
OpCode::PolarRepeat => {
let half = child.half_size();
let center = child.center();
let max_r = half.x.max(half.z) + center.x.abs().max(center.z.abs());
AabbPacked::new(
Vec3::new(-max_r, child.min().y, -max_r),
Vec3::new(max_r, child.max().y, max_r),
)
}
OpCode::Revolution => {
let offset = params[0];
let max_r = child.max().x.abs().max(child.min().x.abs()) + offset.abs();
AabbPacked::new(
Vec3::new(-max_r, child.min().y, -max_r),
Vec3::new(max_r, child.max().y, max_r),
)
}
OpCode::Extrude => {
let hh = params[0];
AabbPacked::new(
Vec3::new(child.min().x, child.min().y, -hh),
Vec3::new(child.max().x, child.max().y, hh),
)
}
OpCode::SweepBezier => {
let bmin_x = params[0].min(params[2]).min(params[4]);
let bmax_x = params[0].max(params[2]).max(params[4]);
let bmin_z = params[1].min(params[3]).min(params[5]);
let bmax_z = params[1].max(params[3]).max(params[5]);
let max_perp = child.max().x.abs().max(child.min().x.abs());
AabbPacked::new(
Vec3::new(bmin_x - max_perp, child.min().y, bmin_z - max_perp),
Vec3::new(bmax_x + max_perp, child.max().y, bmax_z + max_perp),
)
}
OpCode::OctantMirror | OpCode::IcosahedralSymmetry => {
let cmin = child.min();
let cmax = child.max();
let m = cmax
.x
.abs()
.max(cmin.x.abs())
.max(cmax.y.abs())
.max(cmin.y.abs())
.max(cmax.z.abs())
.max(cmin.z.abs());
let r = if opcode == OpCode::IcosahedralSymmetry {
m * 3f32.sqrt()
} else {
m
};
AabbPacked::new(Vec3::splat(-r), Vec3::splat(r))
}
OpCode::HeightmapDisplacement => child.expand(params[0].abs()),
OpCode::SurfaceRoughness => child.expand(params[1].abs()),
OpCode::Animated => child,
OpCode::ProjectiveTransform | OpCode::LatticeDeform | OpCode::SdfSkinning | OpCode::IFS => {
AabbPacked::infinite()
}
_ => unreachable!("transform_or_modifier_aabb called with non-prefix {opcode:?}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compiled::CompiledSdfBvh;
use crate::types::SdfNode;
fn build_two_sphere_union() -> (SdfNode, CompiledSdfBvh) {
let a = SdfNode::sphere(1.0);
let b = SdfNode::sphere(0.5).translate(3.0, 0.0, 0.0);
let scene = a.union(b);
let bvh = CompiledSdfBvh::compile(&scene);
(scene, bvh)
}
#[test]
fn refit_all_produces_same_aabbs_as_fresh_compile() {
let (scene, mut bvh) = build_two_sphere_union();
let baseline = CompiledSdfBvh::compile(&scene);
let count = refit_all(&mut bvh).expect("refit_all must succeed");
assert!(count > 0);
for (a, b) in bvh.aabbs.iter().zip(baseline.aabbs.iter()) {
assert!((a.min() - b.min()).length() < 1e-4);
assert!((a.max() - b.max()).length() < 1e-4);
}
assert!((bvh.scene_aabb.min() - baseline.scene_aabb.min()).length() < 1e-4);
}
#[test]
fn refit_all_reflects_param_mutation() {
let (_, mut bvh) = build_two_sphere_union();
let sphere_idx = bvh
.instructions
.iter()
.position(|i| i.opcode == OpCode::Sphere)
.unwrap();
bvh.instructions[sphere_idx].params[0] = 4.0;
refit_all(&mut bvh).unwrap();
let aabb = bvh.aabbs[sphere_idx];
assert!((aabb.max() - Vec3::splat(4.0)).length() < 1e-4);
assert!((aabb.min() - Vec3::splat(-4.0)).length() < 1e-4);
}
#[test]
fn refit_all_rejects_malformed_bytecode() {
let (_, mut bvh) = build_two_sphere_union();
bvh.instructions[0].opcode = OpCode::ProjectiveTransform;
let err = refit_all(&mut bvh).expect_err("dangling prefix transform must fail refit");
assert!(
matches!(
err,
RefitError::ValueStackUnderflow { .. } | RefitError::TransformStackUnderflow { .. }
),
"expected a stack-shape error, got {err:?}"
);
}
#[test]
fn refit_partial_recomputes_only_ancestor_chain() {
let (_, mut bvh) = build_two_sphere_union();
let baseline_count = refit_all(&mut bvh).unwrap();
assert!(baseline_count > 0);
let sphere_idx = bvh
.instructions
.iter()
.position(|i| i.opcode == OpCode::Sphere)
.unwrap();
let partial_count = refit_partial(&mut bvh, &[sphere_idx]).unwrap();
assert!(partial_count > 0);
assert!(
partial_count < baseline_count,
"partial refit should recompute fewer instructions than full ({partial_count} vs {baseline_count})"
);
}
#[test]
fn refit_partial_matches_refit_all_when_dirty_is_everything() {
let (scene, mut bvh_partial) = build_two_sphere_union();
let mut bvh_full = CompiledSdfBvh::compile(&scene);
let all_indices: Vec<usize> = (0..bvh_partial.instructions.len()).collect();
refit_partial(&mut bvh_partial, &all_indices).unwrap();
refit_all(&mut bvh_full).unwrap();
for (a, b) in bvh_partial.aabbs.iter().zip(bvh_full.aabbs.iter()) {
assert!((a.min() - b.min()).length() < 1e-4);
assert!((a.max() - b.max()).length() < 1e-4);
}
}
#[test]
fn refit_partial_rejects_out_of_bounds_dirty() {
let (_, mut bvh) = build_two_sphere_union();
let err = refit_partial(&mut bvh, &[9999]).expect_err("out of bounds must fail");
assert!(matches!(err, RefitError::DirtyIndexOutOfBounds { .. }));
}
#[test]
fn build_parent_indices_root_has_no_parent() {
let (_, bvh) = build_two_sphere_union();
let parents = build_parent_indices(&bvh).unwrap();
let union_idx = bvh
.instructions
.iter()
.position(|i| i.opcode == OpCode::Union)
.unwrap();
assert_eq!(parents[union_idx], None);
}
#[test]
fn build_parent_indices_child_points_to_parent_op() {
let scene = SdfNode::sphere(1.0).union(SdfNode::sphere(0.5));
let bvh = CompiledSdfBvh::compile(&scene);
let parents = build_parent_indices(&bvh).unwrap();
let union_idx = bvh
.instructions
.iter()
.position(|i| i.opcode == OpCode::Union)
.unwrap();
let first_sphere = bvh
.instructions
.iter()
.position(|i| i.opcode == OpCode::Sphere)
.unwrap();
assert_eq!(parents[first_sphere], Some(union_idx as u32));
}
fn compile_and_refit(scene: SdfNode) -> Result<CompiledSdfBvh, RefitError> {
let mut bvh = CompiledSdfBvh::compile(&scene);
refit_all(&mut bvh)?;
Ok(bvh)
}
#[test]
fn refit_supports_twist_modifier() {
let scene = SdfNode::sphere(1.0).twist(0.5);
let bvh = compile_and_refit(scene).expect("Twist refit must succeed");
assert!(bvh.scene_aabb.is_valid());
}
#[test]
fn refit_supports_bend_modifier() {
let scene = SdfNode::box3d(1.0, 1.0, 1.0).bend(0.3);
let bvh = compile_and_refit(scene).expect("Bend refit must succeed");
assert!(bvh.scene_aabb.is_valid());
}
#[test]
fn refit_supports_repeat_finite_modifier() {
let scene = SdfNode::sphere(0.5).repeat_finite([2, 2, 2], Vec3::splat(1.5));
let bvh = compile_and_refit(scene).expect("RepeatFinite refit must succeed");
assert!(bvh.scene_aabb.is_valid());
assert!(bvh.scene_aabb.half_size().max_element() > 0.5);
}
#[test]
fn refit_supports_repeat_infinite_modifier() {
let scene = SdfNode::sphere(0.5).repeat_infinite(2.0, 2.0, 2.0);
let bvh = compile_and_refit(scene).expect("RepeatInfinite refit must succeed");
assert_eq!(
bvh.scene_aabb.min().x,
f32::MIN,
"RepeatInfinite must yield an infinite scene AABB"
);
}
#[test]
fn refit_supports_onion_and_displacement_modifiers() {
let scene = SdfNode::sphere(1.0).onion(0.1);
let bvh = compile_and_refit(scene).expect("Onion refit must succeed");
assert!(bvh.scene_aabb.half_size().max_element() >= 1.1);
let scene = SdfNode::sphere(1.0).displacement(0.2);
let bvh = compile_and_refit(scene).expect("Displacement refit must succeed");
assert!(bvh.scene_aabb.half_size().max_element() >= 1.0);
}
#[test]
fn refit_supports_elongate_modifier() {
let scene = SdfNode::sphere(0.5).elongate(1.0, 0.0, 0.0);
let bvh = compile_and_refit(scene).expect("Elongate refit must succeed");
assert!(bvh.scene_aabb.max().x >= 1.5);
}
#[test]
fn refit_supports_mirror_modifier() {
let scene = SdfNode::sphere(0.5)
.translate(2.0, 0.0, 0.0)
.mirror(true, false, false);
let bvh = compile_and_refit(scene).expect("Mirror refit must succeed");
assert!(bvh.scene_aabb.is_valid());
}
#[test]
fn refit_supports_extrude_and_revolution() {
let scene = SdfNode::sphere(0.5).extrude(1.0);
let bvh = compile_and_refit(scene).expect("Extrude refit must succeed");
assert!(bvh.scene_aabb.is_valid());
let scene = SdfNode::sphere(0.5).revolution(2.0);
let bvh = compile_and_refit(scene).expect("Revolution refit must succeed");
assert!(bvh.scene_aabb.max().x >= 2.0);
}
#[test]
fn refit_supports_polar_repeat_modifier() {
let scene = SdfNode::sphere(0.5)
.translate(1.5, 0.0, 0.0)
.polar_repeat(6);
let bvh = compile_and_refit(scene).expect("PolarRepeat refit must succeed");
assert!(bvh.scene_aabb.is_valid());
}
#[test]
fn refit_supports_chamfer_union() {
let a = SdfNode::sphere(1.0);
let b = SdfNode::sphere(0.5).translate(1.5, 0.0, 0.0);
let scene = a.chamfer_union(b, 0.2);
let bvh = compile_and_refit(scene).expect("ChamferUnion refit must succeed");
assert!(bvh.scene_aabb.is_valid());
}
#[test]
fn refit_supports_stairs_union() {
let a = SdfNode::sphere(1.0);
let b = SdfNode::sphere(0.5).translate(1.5, 0.0, 0.0);
let scene = a.stairs_union(b, 0.15, 3.0);
let bvh = compile_and_refit(scene).expect("StairsUnion refit must succeed");
assert!(bvh.scene_aabb.is_valid());
}
#[test]
fn refit_supports_columns_pipe_engrave_variants() {
let a = SdfNode::sphere(1.0);
let b = SdfNode::sphere(0.5).translate(1.5, 0.0, 0.0);
let scene = a.clone().columns_union(b.clone(), 0.2, 3.0);
compile_and_refit(scene).expect("ColumnsUnion refit must succeed");
let scene = a.clone().pipe(b.clone(), 0.1);
compile_and_refit(scene).expect("Pipe refit must succeed");
let scene = a.engrave(b, 0.15);
compile_and_refit(scene).expect("Engrave refit must succeed");
}
#[test]
fn refit_supports_exp_smooth_union_intersection_subtraction() {
let a = SdfNode::sphere(1.0);
let b = SdfNode::sphere(0.5).translate(1.5, 0.0, 0.0);
compile_and_refit(a.clone().exp_smooth_union(b.clone(), 0.2))
.expect("ExpSmoothUnion refit must succeed");
compile_and_refit(a.clone().exp_smooth_intersection(b.clone(), 0.2))
.expect("ExpSmoothIntersection refit must succeed");
compile_and_refit(a.exp_smooth_subtract(b, 0.2))
.expect("ExpSmoothSubtraction refit must succeed");
}
#[test]
fn refit_partial_walks_ancestor_chain_with_modifiers() {
let scene = SdfNode::sphere(1.0).twist(0.5).bend(0.3);
let mut bvh = CompiledSdfBvh::compile(&scene);
let sphere_idx = bvh
.instructions
.iter()
.position(|i| i.opcode == OpCode::Sphere)
.unwrap();
let count = refit_partial(&mut bvh, &[sphere_idx]).unwrap();
assert_eq!(count, 3);
}
#[test]
fn subtree_end_populates_on_compile() {
let (_, bvh) = build_two_sphere_union();
assert_eq!(
bvh.subtree_end.len(),
bvh.instructions.len(),
"subtree_end must cover every instruction"
);
let translate_idx = bvh
.instructions
.iter()
.position(|i| i.opcode == OpCode::Translate);
if let Some(idx) = translate_idx {
let end = bvh.subtree_end[idx] as usize;
assert!(end > idx, "transform subtree_end must be > its own index");
assert_eq!(bvh.instructions[end].opcode, OpCode::PopTransform);
}
}
#[test]
fn refit_partial_skips_untouched_transform_subtree() {
let a = SdfNode::sphere(1.0);
let b = SdfNode::box3d(0.5, 0.5, 0.5).translate(3.0, 0.0, 0.0);
let scene = a.union(b);
let mut bvh = CompiledSdfBvh::compile(&scene);
let sphere_idx = bvh
.instructions
.iter()
.position(|i| i.opcode == OpCode::Sphere)
.unwrap();
bvh.instructions[sphere_idx].params[0] = 2.0;
let count = refit_partial(&mut bvh, &[sphere_idx]).unwrap();
assert_eq!(
count, 2,
"only dirty leaf + Union ancestor should recompute"
);
let sphere_aabb = bvh.aabbs[sphere_idx];
assert!((sphere_aabb.max().x - 2.0).abs() < 1e-4);
}
#[test]
fn refit_all_matches_baseline_for_extended_opcodes() {
let leaf = SdfNode::sphere(1.0);
let cube = SdfNode::box3d(0.5, 0.5, 0.5).translate(2.0, 0.0, 0.0);
let cases: Vec<(&str, SdfNode)> = vec![
(
"chamfer_union",
leaf.clone().chamfer_union(cube.clone(), 0.2),
),
(
"chamfer_intersection",
leaf.clone().chamfer_intersection(cube.clone(), 0.2),
),
(
"chamfer_subtraction",
leaf.clone().chamfer_subtract(cube.clone(), 0.2),
),
(
"stairs_union",
leaf.clone().stairs_union(cube.clone(), 0.15, 3.0),
),
(
"columns_union",
leaf.clone().columns_union(cube.clone(), 0.2, 3.0),
),
("pipe", leaf.clone().pipe(cube.clone(), 0.1)),
("engrave", leaf.clone().engrave(cube.clone(), 0.15)),
("groove", leaf.clone().groove(cube.clone(), 0.1, 0.05)),
("tongue", leaf.clone().tongue(cube.clone(), 0.1, 0.05)),
(
"exp_smooth_union",
leaf.clone().exp_smooth_union(cube.clone(), 0.2),
),
("twist", leaf.clone().twist(0.5)),
("bend", leaf.clone().bend(0.3)),
("onion", leaf.clone().onion(0.1)),
("displacement", leaf.clone().displacement(0.2)),
("noise", leaf.clone().noise(0.1, 2.0, 42)),
("elongate", leaf.clone().elongate(1.0, 0.0, 0.0)),
(
"mirror",
leaf.clone()
.translate(2.0, 0.0, 0.0)
.mirror(true, false, false),
),
(
"repeat_finite",
leaf.clone().repeat_finite([2, 2, 2], Vec3::splat(1.5)),
),
("shear", leaf.clone().shear(0.1, 0.0, 0.0)),
("taper", leaf.clone().taper(0.3)),
("extrude", leaf.clone().extrude(1.0)),
("revolution", leaf.clone().revolution(2.0)),
(
"polar_repeat",
leaf.clone().translate(1.5, 0.0, 0.0).polar_repeat(6),
),
];
for (name, scene) in cases {
let baseline = CompiledSdfBvh::compile(&scene);
let mut refit = baseline.clone();
refit_all(&mut refit).unwrap_or_else(|e| {
panic!("refit_all failed for {name}: {e:?}");
});
for (i, (a, b)) in refit.aabbs.iter().zip(baseline.aabbs.iter()).enumerate() {
let dmin = (a.min() - b.min()).length();
let dmax = (a.max() - b.max()).length();
assert!(
dmin < 1e-3 && dmax < 1e-3,
"case {name}: instruction {i} AABB drift min={dmin} max={dmax} \
(refit min={:?} baseline min={:?})",
a.min(),
b.min(),
);
}
}
}
#[test]
fn refit_partial_deeply_nested_skips_intermediate_subtrees() {
let a = SdfNode::sphere(1.0);
let b = SdfNode::sphere(0.5).translate(3.0, 0.0, 0.0);
let c = SdfNode::sphere(0.4).translate(0.0, 4.0, 0.0);
let d = SdfNode::box3d(0.3, 0.3, 0.3).translate(0.0, 0.0, 5.0);
let scene = a.union(b).union(c).union(d);
let mut bvh_partial = CompiledSdfBvh::compile(&scene);
let mut bvh_full = CompiledSdfBvh::compile(&scene);
let sphere_idx = bvh_partial
.instructions
.iter()
.position(|i| i.opcode == OpCode::Sphere)
.unwrap();
bvh_partial.instructions[sphere_idx].params[0] = 2.5;
bvh_full.instructions[sphere_idx].params[0] = 2.5;
let partial_count = refit_partial(&mut bvh_partial, &[sphere_idx]).unwrap();
let full_count = refit_all(&mut bvh_full).unwrap();
assert_eq!(
partial_count, 4,
"deep partial should recompute only ancestor chain (Sphere + 3 Union), got {}",
partial_count
);
assert!(
full_count > partial_count,
"full refit ({full_count}) must recompute more than partial ({partial_count})"
);
assert!(
(bvh_partial.scene_aabb.max() - bvh_full.scene_aabb.max()).length() < 1e-4,
"scene AABB agreement across deeply nested partial vs full"
);
}
#[test]
fn refit_partial_still_produces_correct_scene_aabb() {
let a = SdfNode::sphere(1.0);
let b = SdfNode::box3d(0.5, 0.5, 0.5).translate(3.0, 0.0, 0.0);
let c = SdfNode::sphere(0.7).translate(0.0, 4.0, 0.0);
let scene = a.union(b).union(c);
let mut bvh_partial = CompiledSdfBvh::compile(&scene);
let mut bvh_full = CompiledSdfBvh::compile(&scene);
let sphere_idx = bvh_partial
.instructions
.iter()
.position(|i| i.opcode == OpCode::Sphere)
.unwrap();
bvh_partial.instructions[sphere_idx].params[0] = 1.5;
bvh_full.instructions[sphere_idx].params[0] = 1.5;
refit_partial(&mut bvh_partial, &[sphere_idx]).unwrap();
refit_all(&mut bvh_full).unwrap();
assert!((bvh_partial.aabbs[sphere_idx].max().x - 1.5).abs() < 1e-4);
assert!(
(bvh_partial.scene_aabb.max() - bvh_full.scene_aabb.max()).length() < 1e-4,
"scene AABB must agree"
);
}
}