use std::sync::Arc;
use alice_sdf::interval::{eval_interval, Interval, Vec3Interval};
use alice_sdf::SdfNode;
use glam::Vec3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CellKind {
Outside,
Inside,
Crossing,
}
#[derive(Debug, Clone)]
pub struct CellInfo {
pub ix: usize,
pub iy: usize,
pub iz: usize,
pub bounds: Vec3Interval,
pub kind: CellKind,
pub pruned_node: Option<SdfNode>,
}
#[derive(Debug, Clone)]
pub struct PruneConfig {
pub aabb_min: Vec3,
pub aabb_max: Vec3,
pub grid_resolution: usize,
}
impl Default for PruneConfig {
fn default() -> Self {
Self {
aabb_min: Vec3::splat(-5.0),
aabb_max: Vec3::splat(5.0),
grid_resolution: 4,
}
}
}
#[derive(Debug, Clone)]
pub struct PruneResult {
pub cells: Vec<CellInfo>,
pub config: PruneConfig,
pub outside_count: usize,
pub inside_count: usize,
pub crossing_count: usize,
}
#[must_use]
pub fn analyze_grid(node: &SdfNode, config: &PruneConfig) -> PruneResult {
let n = config.grid_resolution;
let total = n * n * n;
let mut cells = Vec::with_capacity(total);
let mut outside_count = 0usize;
let mut inside_count = 0usize;
let mut crossing_count = 0usize;
let extent = config.aabb_max - config.aabb_min;
#[allow(clippy::cast_precision_loss)]
let inv_n = 1.0 / n as f32;
for iz in 0..n {
for iy in 0..n {
for ix in 0..n {
#[allow(clippy::cast_precision_loss)]
let lo =
config.aabb_min + extent * Vec3::new(ix as f32, iy as f32, iz as f32) * inv_n;
#[allow(clippy::cast_precision_loss)]
let hi = config.aabb_min
+ extent * Vec3::new((ix + 1) as f32, (iy + 1) as f32, (iz + 1) as f32) * inv_n;
let bounds = Vec3Interval {
x: Interval { lo: lo.x, hi: hi.x },
y: Interval { lo: lo.y, hi: hi.y },
z: Interval { lo: lo.z, hi: hi.z },
};
let iv = eval_interval(node, bounds);
let kind = if iv.lo > 0.0 {
outside_count += 1;
CellKind::Outside
} else if iv.hi < 0.0 {
inside_count += 1;
CellKind::Inside
} else {
crossing_count += 1;
CellKind::Crossing
};
let pruned_node = if kind == CellKind::Crossing {
Some(prune_subtree(node, bounds))
} else {
None
};
cells.push(CellInfo {
ix,
iy,
iz,
bounds,
kind,
pruned_node,
});
}
}
}
PruneResult {
cells,
config: config.clone(),
outside_count,
inside_count,
crossing_count,
}
}
#[must_use]
#[allow(clippy::too_many_lines)]
fn prune_subtree(node: &SdfNode, bounds: Vec3Interval) -> SdfNode {
match node {
SdfNode::Union { a, b } => {
let iv_a = eval_interval(a, bounds);
let iv_b = eval_interval(b, bounds);
if iv_a.lo >= iv_b.hi {
prune_subtree(b, bounds)
} else if iv_b.lo >= iv_a.hi {
prune_subtree(a, bounds)
} else {
SdfNode::Union {
a: Arc::new(prune_subtree(a, bounds)),
b: Arc::new(prune_subtree(b, bounds)),
}
}
}
SdfNode::SmoothUnion { a, b, k } => {
let iv_a = eval_interval(a, bounds);
let iv_b = eval_interval(b, bounds);
if iv_a.lo >= iv_b.hi + k {
prune_subtree(b, bounds)
} else if iv_b.lo >= iv_a.hi + k {
prune_subtree(a, bounds)
} else {
SdfNode::SmoothUnion {
a: Arc::new(prune_subtree(a, bounds)),
b: Arc::new(prune_subtree(b, bounds)),
k: *k,
}
}
}
SdfNode::Intersection { a, b } => SdfNode::Intersection {
a: Arc::new(prune_subtree(a, bounds)),
b: Arc::new(prune_subtree(b, bounds)),
},
SdfNode::SmoothIntersection { a, b, k } => SdfNode::SmoothIntersection {
a: Arc::new(prune_subtree(a, bounds)),
b: Arc::new(prune_subtree(b, bounds)),
k: *k,
},
SdfNode::Subtraction { a, b } => SdfNode::Subtraction {
a: Arc::new(prune_subtree(a, bounds)),
b: Arc::new(prune_subtree(b, bounds)),
},
SdfNode::SmoothSubtraction { a, b, k } => SdfNode::SmoothSubtraction {
a: Arc::new(prune_subtree(a, bounds)),
b: Arc::new(prune_subtree(b, bounds)),
k: *k,
},
SdfNode::Translate { child, offset } => {
let shifted = Vec3Interval {
x: Interval {
lo: bounds.x.lo - offset.x,
hi: bounds.x.hi - offset.x,
},
y: Interval {
lo: bounds.y.lo - offset.y,
hi: bounds.y.hi - offset.y,
},
z: Interval {
lo: bounds.z.lo - offset.z,
hi: bounds.z.hi - offset.z,
},
};
SdfNode::Translate {
child: Arc::new(prune_subtree(child, shifted)),
offset: *offset,
}
}
SdfNode::Rotate { child, rotation } => SdfNode::Rotate {
child: Arc::new(prune_subtree(child, bounds)),
rotation: *rotation,
},
SdfNode::Scale { child, factor } => SdfNode::Scale {
child: Arc::new(prune_subtree(child, bounds)),
factor: *factor,
},
SdfNode::Twist { child, strength } => SdfNode::Twist {
child: Arc::new(prune_subtree(child, bounds)),
strength: *strength,
},
SdfNode::Bend { child, curvature } => SdfNode::Bend {
child: Arc::new(prune_subtree(child, bounds)),
curvature: *curvature,
},
SdfNode::Round { child, radius } => SdfNode::Round {
child: Arc::new(prune_subtree(child, bounds)),
radius: *radius,
},
SdfNode::Onion { child, thickness } => SdfNode::Onion {
child: Arc::new(prune_subtree(child, bounds)),
thickness: *thickness,
},
SdfNode::Mirror { child, axes } => SdfNode::Mirror {
child: Arc::new(prune_subtree(child, bounds)),
axes: *axes,
},
SdfNode::RepeatInfinite { child, spacing } => SdfNode::RepeatInfinite {
child: Arc::new(prune_subtree(child, bounds)),
spacing: *spacing,
},
SdfNode::Animated {
child,
speed,
amplitude,
} => SdfNode::Animated {
child: Arc::new(prune_subtree(child, bounds)),
speed: *speed,
amplitude: *amplitude,
},
SdfNode::Morph { a, b, t } => SdfNode::Morph {
a: Arc::new(prune_subtree(a, bounds)),
b: Arc::new(prune_subtree(b, bounds)),
t: *t,
},
other => other.clone(),
}
}
#[must_use]
pub fn count_nodes(node: &SdfNode) -> usize {
match node {
SdfNode::Union { a, b }
| SdfNode::Intersection { a, b }
| SdfNode::Subtraction { a, b }
| SdfNode::SmoothUnion { a, b, .. }
| SdfNode::SmoothIntersection { a, b, .. }
| SdfNode::SmoothSubtraction { a, b, .. }
| SdfNode::Morph { a, b, .. } => 1 + count_nodes(a) + count_nodes(b),
SdfNode::Translate { child, .. }
| SdfNode::Rotate { child, .. }
| SdfNode::Scale { child, .. }
| SdfNode::Twist { child, .. }
| SdfNode::Bend { child, .. }
| SdfNode::Round { child, .. }
| SdfNode::Onion { child, .. }
| SdfNode::Mirror { child, .. }
| SdfNode::RepeatInfinite { child, .. }
| SdfNode::Animated { child, .. } => 1 + count_nodes(child),
_ => 1,
}
}
#[must_use]
#[cfg(feature = "glsl")]
pub fn to_pruned_glsl(node: &SdfNode, result: &PruneResult) -> String {
use std::fmt::Write;
use alice_sdf::compiled::glsl::{GlslShader, GlslTranspileMode};
let mut out = String::with_capacity(4096);
let full = GlslShader::transpile(node, GlslTranspileMode::Hardcoded);
out.push_str("// ── Full scene (fallback) ──\n");
out.push_str(&full.source);
out.push('\n');
for cell in &result.cells {
if cell.kind != CellKind::Crossing {
continue;
}
let Some(ref pruned) = cell.pruned_node else {
continue;
};
let cell_glsl = GlslShader::transpile(pruned, GlslTranspileMode::Hardcoded);
let _ = write!(
out,
"\n// ── Cell ({}, {}, {}) ──\n",
cell.ix, cell.iy, cell.iz
);
let cell_fn = format!("sdf_cell_{}_{}_{}", cell.ix, cell.iy, cell.iz);
let renamed = cell_glsl
.source
.replace("float sdf(", &format!("float {cell_fn}("));
out.push_str(&renamed);
out.push('\n');
}
let n = result.config.grid_resolution;
let mn = result.config.aabb_min;
let mx = result.config.aabb_max;
out.push_str("\n// ── Dispatcher ──\n");
out.push_str("float sdf_pruned(vec3 p) {\n");
let _ = writeln!(
out,
" vec3 rel = (p - vec3({:.6}, {:.6}, {:.6})) / vec3({:.6}, {:.6}, {:.6});",
mn.x,
mn.y,
mn.z,
mx.x - mn.x,
mx.y - mn.y,
mx.z - mn.z,
);
#[allow(clippy::cast_precision_loss)]
let _ = writeln!(
out,
" ivec3 idx = clamp(ivec3(rel * {:.1}), ivec3(0), ivec3({}));",
n as f32,
n - 1,
);
let _ = writeln!(
out,
" int cell = idx.z * {} + idx.y * {} + idx.x;",
n * n,
n
);
out.push_str(" switch(cell) {\n");
for cell in &result.cells {
if cell.kind != CellKind::Crossing {
continue;
}
let idx = cell.iz * n * n + cell.iy * n + cell.ix;
let _ = writeln!(
out,
" case {idx}: return sdf_cell_{}_{}_{vz}(p);",
cell.ix,
cell.iy,
vz = cell.iz,
);
}
out.push_str(" default: return sdf(p);\n");
out.push_str(" }\n");
out.push_str("}\n");
out
}
#[must_use]
pub fn summary(result: &PruneResult) -> String {
let total = result.cells.len();
format!(
"Grid {}^3 = {} cells: {} outside, {} inside, {} crossing",
result.config.grid_resolution,
total,
result.outside_count,
result.inside_count,
result.crossing_count,
)
}