use alice_sdf::interval::{Interval, Vec3Interval};
use alice_sdf::SdfNode;
use glam::Vec3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Priority {
Hard,
Soft(f32),
}
#[derive(Debug, Clone)]
pub enum Constraint {
NonOverlap {
a: SdfNode,
b: SdfNode,
},
Containment {
inner: SdfNode,
outer: SdfNode,
},
MinThickness {
node: SdfNode,
min_thickness: f32,
},
Stress {
node: SdfNode,
load_points: Vec<(Vec3, f32)>,
min_thickness_factor: f32,
},
Thermal {
node: SdfNode,
heat_sources: Vec<Vec3>,
search_radius: f32,
min_surface_ratio: f32,
},
Contact {
a: SdfNode,
b: SdfNode,
min_distance: f32,
max_distance: f32,
},
Continuity {
node: SdfNode,
seed_point: Vec3,
},
VolumeConservation {
before: SdfNode,
after: SdfNode,
relative_tolerance: f32,
},
}
#[derive(Debug, Clone)]
pub struct Law {
pub name: String,
pub priority: Priority,
pub constraint: Constraint,
}
impl Law {
#[must_use]
pub fn hard(name: impl Into<String>, constraint: Constraint) -> Self {
Self {
name: name.into(),
priority: Priority::Hard,
constraint,
}
}
#[must_use]
pub fn soft(name: impl Into<String>, weight: f32, constraint: Constraint) -> Self {
Self {
name: name.into(),
priority: Priority::Soft(weight),
constraint,
}
}
}
#[derive(Debug, Clone)]
pub struct Violation {
pub law_name: String,
pub priority: Priority,
pub residual: f32,
pub point: Vec3,
pub region: Vec3Interval,
}
#[derive(Debug, Clone)]
pub struct LawReport {
pub total_laws: usize,
pub passed: usize,
pub violations: Vec<Violation>,
}
impl LawReport {
#[must_use]
pub const fn all_passed(&self) -> bool {
self.violations.is_empty()
}
#[must_use]
pub fn has_hard_violations(&self) -> bool {
self.violations.iter().any(|v| v.priority == Priority::Hard)
}
}
#[derive(Debug, Clone)]
pub struct CheckConfig {
pub aabb_min: Vec3,
pub aabb_max: Vec3,
pub resolution: usize,
}
impl Default for CheckConfig {
fn default() -> Self {
Self {
aabb_min: Vec3::splat(-5.0),
aabb_max: Vec3::splat(5.0),
resolution: 8,
}
}
}
fn sdf_eval(node: &SdfNode, point: Vec3) -> f32 {
alice_sdf::eval(node, point)
}
struct GridSampler {
aabb_min: Vec3,
step: Vec3,
half_step: Vec3,
n: usize,
ix: usize,
iy: usize,
iz: usize,
}
impl GridSampler {
fn new(config: &CheckConfig) -> Self {
let n = config.resolution;
let extent = config.aabb_max - config.aabb_min;
#[allow(clippy::cast_precision_loss)]
let step = extent / n as f32;
Self {
aabb_min: config.aabb_min,
step,
half_step: step * 0.5,
n,
ix: 0,
iy: 0,
iz: 0,
}
}
}
impl Iterator for GridSampler {
type Item = (Vec3, Vec3Interval);
fn next(&mut self) -> Option<Self::Item> {
if self.iz >= self.n {
return None;
}
#[allow(clippy::cast_precision_loss)]
let lo =
self.aabb_min + self.step * Vec3::new(self.ix as f32, self.iy as f32, self.iz as f32);
let center = lo + self.half_step;
let hi = lo + self.step;
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 },
};
self.ix += 1;
if self.ix >= self.n {
self.ix = 0;
self.iy += 1;
if self.iy >= self.n {
self.iy = 0;
self.iz += 1;
}
}
Some((center, bounds))
}
}
#[must_use]
pub fn check_laws(laws: &[Law], config: &CheckConfig) -> LawReport {
let mut violations = Vec::new();
for law in laws {
let violation = match &law.constraint {
Constraint::NonOverlap { a, b } => {
check_non_overlap(a, b, &law.name, law.priority, config)
}
Constraint::Containment { inner, outer } => {
check_containment(inner, outer, &law.name, law.priority, config)
}
Constraint::MinThickness {
node,
min_thickness,
} => check_min_thickness(node, *min_thickness, &law.name, law.priority, config),
Constraint::Stress {
node,
load_points,
min_thickness_factor,
} => check_stress(
node,
load_points,
*min_thickness_factor,
&law.name,
law.priority,
config,
),
Constraint::Thermal {
node,
heat_sources,
search_radius,
min_surface_ratio,
} => check_thermal(
node,
heat_sources,
*search_radius,
*min_surface_ratio,
&law.name,
law.priority,
config,
),
Constraint::Contact {
a,
b,
min_distance,
max_distance,
} => check_contact(
a,
b,
*min_distance,
*max_distance,
&law.name,
law.priority,
config,
),
Constraint::Continuity { node, seed_point } => {
check_continuity(node, *seed_point, &law.name, law.priority, config)
}
Constraint::VolumeConservation {
before,
after,
relative_tolerance,
} => check_volume_conservation(
before,
after,
*relative_tolerance,
&law.name,
law.priority,
config,
),
};
if let Some(v) = violation {
violations.push(v);
}
}
violations.sort_by(|a, b| {
b.residual
.abs()
.partial_cmp(&a.residual.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
let passed = laws.len() - violations.len();
LawReport {
total_laws: laws.len(),
passed,
violations,
}
}
fn check_non_overlap(
a: &SdfNode,
b: &SdfNode,
law_name: &str,
priority: Priority,
config: &CheckConfig,
) -> Option<Violation> {
let mut worst: Option<(f32, Vec3, Vec3Interval)> = None;
for (center, bounds) in GridSampler::new(config) {
let da = sdf_eval(a, center);
let db = sdf_eval(b, center);
if da < 0.0 && db < 0.0 {
let residual = da.max(db); match &worst {
Some((w, _, _)) if residual >= *w => {}
_ => worst = Some((residual, center, bounds)),
}
}
}
worst.map(|(residual, point, region)| Violation {
law_name: law_name.to_string(),
priority,
residual,
point,
region,
})
}
fn check_containment(
inner: &SdfNode,
outer: &SdfNode,
law_name: &str,
priority: Priority,
config: &CheckConfig,
) -> Option<Violation> {
let mut worst: Option<(f32, Vec3, Vec3Interval)> = None;
for (center, bounds) in GridSampler::new(config) {
let d_inner = sdf_eval(inner, center);
let d_outer = sdf_eval(outer, center);
if d_inner < 0.0 && d_outer > 0.0 {
let residual = -d_outer; match &worst {
Some((w, _, _)) if residual >= *w => {}
_ => worst = Some((residual, center, bounds)),
}
}
}
worst.map(|(residual, point, region)| Violation {
law_name: law_name.to_string(),
priority,
residual,
point,
region,
})
}
fn check_min_thickness(
node: &SdfNode,
min_thickness: f32,
law_name: &str,
priority: Priority,
config: &CheckConfig,
) -> Option<Violation> {
let mut worst: Option<(f32, Vec3, Vec3Interval)> = None;
for (center, bounds) in GridSampler::new(config) {
let d = sdf_eval(node, center);
if d < 0.0 && d.abs() < min_thickness {
let residual = d.abs() - min_thickness; match &worst {
Some((w, _, _)) if residual >= *w => {}
_ => worst = Some((residual, center, bounds)),
}
}
}
worst.map(|(residual, point, region)| Violation {
law_name: law_name.to_string(),
priority,
residual,
point,
region,
})
}
fn check_stress(
node: &SdfNode,
load_points: &[(Vec3, f32)],
min_thickness_factor: f32,
law_name: &str,
priority: Priority,
config: &CheckConfig,
) -> Option<Violation> {
let mut worst: Option<(f32, Vec3, Vec3Interval)> = None;
for (center, bounds) in GridSampler::new(config) {
let d = sdf_eval(node, center);
if d >= 0.0 {
continue; }
let thickness = d.abs();
for &(lp, force) in load_points {
let search_radius = force.max(1.0);
if center.distance(lp) > search_radius {
continue;
}
let required_thickness = force * min_thickness_factor;
if thickness < required_thickness {
let residual = thickness - required_thickness; match &worst {
Some((w, _, _)) if residual >= *w => {}
_ => worst = Some((residual, center, bounds)),
}
}
}
}
worst.map(|(residual, point, region)| Violation {
law_name: law_name.to_string(),
priority,
residual,
point,
region,
})
}
fn check_thermal(
node: &SdfNode,
heat_sources: &[Vec3],
search_radius: f32,
min_surface_ratio: f32,
law_name: &str,
priority: Priority,
config: &CheckConfig,
) -> Option<Violation> {
let extent = config.aabb_max - config.aabb_min;
#[allow(clippy::cast_precision_loss)]
let step = extent.x / (config.resolution as f32);
let surface_threshold = step;
let mut worst: Option<(f32, Vec3, Vec3Interval)> = None;
for &source in heat_sources {
let mut surface_count: usize = 0;
let mut interior_count: usize = 0;
let mut worst_cell: Option<(Vec3, Vec3Interval)> = None;
for (center, bounds) in GridSampler::new(config) {
if center.distance(source) > search_radius {
continue;
}
let d = sdf_eval(node, center);
if d < 0.0 {
interior_count += 1;
if worst_cell.is_none() {
worst_cell = Some((center, bounds));
}
}
if d.abs() < surface_threshold {
surface_count += 1;
}
}
if interior_count == 0 {
continue; }
#[allow(clippy::cast_precision_loss)]
let ratio = (surface_count as f32) / (interior_count as f32);
if ratio < min_surface_ratio {
let residual = ratio - min_surface_ratio; if let Some((point, region)) = worst_cell {
match &worst {
Some((w, _, _)) if residual >= *w => {}
_ => worst = Some((residual, point, region)),
}
}
}
}
worst.map(|(residual, point, region)| Violation {
law_name: law_name.to_string(),
priority,
residual,
point,
region,
})
}
fn check_contact(
a: &SdfNode,
b: &SdfNode,
min_distance: f32,
max_distance: f32,
law_name: &str,
priority: Priority,
config: &CheckConfig,
) -> Option<Violation> {
let mut min_surface_distance: Option<(f32, Vec3, Vec3Interval)> = None;
let mut interfering: Option<(f32, Vec3, Vec3Interval)> = None;
for (center, bounds) in GridSampler::new(config) {
let da = sdf_eval(a, center);
let db = sdf_eval(b, center);
if da < 0.0 && db < 0.0 {
let residual = da.max(db); match &interfering {
Some((w, _, _)) if residual >= *w => {}
_ => interfering = Some((residual, center, bounds)),
}
} else if da > 0.0 && db > 0.0 {
let dist = da + db;
match &min_surface_distance {
Some((w, _, _)) if dist >= *w => {}
_ => min_surface_distance = Some((dist, center, bounds)),
}
}
}
if let Some((residual, point, region)) = interfering {
return Some(Violation {
law_name: law_name.to_string(),
priority,
residual,
point,
region,
});
}
let (min_dist, point, region) = min_surface_distance?;
if min_dist < min_distance {
Some(Violation {
law_name: law_name.to_string(),
priority,
residual: min_dist - min_distance, point,
region,
})
} else if min_dist > max_distance {
Some(Violation {
law_name: law_name.to_string(),
priority,
residual: max_dist_residual(min_dist, max_distance), point,
region,
})
} else {
None
}
}
fn max_dist_residual(actual: f32, limit: f32) -> f32 {
limit - actual
}
fn check_continuity(
node: &SdfNode,
seed_point: Vec3,
law_name: &str,
priority: Priority,
config: &CheckConfig,
) -> Option<Violation> {
let seed_dist = sdf_eval(node, seed_point);
if seed_dist >= 0.0 {
return Some(Violation {
law_name: law_name.to_string(),
priority,
residual: seed_dist, point: seed_point,
region: Vec3Interval {
x: Interval {
lo: seed_point.x,
hi: seed_point.x,
},
y: Interval {
lo: seed_point.y,
hi: seed_point.y,
},
z: Interval {
lo: seed_point.z,
hi: seed_point.z,
},
},
});
}
let n = config.resolution;
let extent = config.aabb_max - config.aabb_min;
#[allow(clippy::cast_precision_loss)]
let step = extent / (n as f32);
let mut interior = vec![false; n * n * n];
let mut total_interior: usize = 0;
for (ix, iy, iz, idx) in grid_indices(n) {
#[allow(clippy::cast_precision_loss)]
let center =
config.aabb_min + step * Vec3::new(ix as f32, iy as f32, iz as f32) + step * 0.5;
if sdf_eval(node, center) < 0.0 {
interior[idx] = true;
total_interior += 1;
}
}
if total_interior == 0 {
return None;
}
let rel = (seed_point - config.aabb_min) / step;
let sx = (rel.x as i32).clamp(0, (n as i32) - 1) as usize;
let sy = (rel.y as i32).clamp(0, (n as i32) - 1) as usize;
let sz = (rel.z as i32).clamp(0, (n as i32) - 1) as usize;
let seed_idx = sx + sy * n + sz * n * n;
if !interior[seed_idx] {
return None;
}
let mut visited = vec![false; n * n * n];
let mut queue = std::collections::VecDeque::new();
queue.push_back((sx, sy, sz));
visited[seed_idx] = true;
let mut reachable: usize = 1;
while let Some((x, y, z)) = queue.pop_front() {
for (dx, dy, dz) in [
(-1_i32, 0_i32, 0_i32),
(1, 0, 0),
(0, -1, 0),
(0, 1, 0),
(0, 0, -1),
(0, 0, 1),
] {
let nx = x as i32 + dx;
let ny = y as i32 + dy;
let nz = z as i32 + dz;
if nx < 0 || nx >= n as i32 || ny < 0 || ny >= n as i32 || nz < 0 || nz >= n as i32 {
continue;
}
let nx = nx as usize;
let ny = ny as usize;
let nz = nz as usize;
let nidx = nx + ny * n + nz * n * n;
if !visited[nidx] && interior[nidx] {
visited[nidx] = true;
reachable += 1;
queue.push_back((nx, ny, nz));
}
}
}
if reachable < total_interior {
for (ix, iy, iz, idx) in grid_indices(n) {
if interior[idx] && !visited[idx] {
#[allow(clippy::cast_precision_loss)]
let lo = config.aabb_min + step * Vec3::new(ix as f32, iy as f32, iz as f32);
let center = lo + step * 0.5;
let hi = lo + step;
#[allow(clippy::cast_precision_loss)]
let unreachable_ratio =
((total_interior - reachable) as f32) / (total_interior as f32);
return Some(Violation {
law_name: law_name.to_string(),
priority,
residual: -unreachable_ratio, point: center,
region: 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 },
},
});
}
}
}
None
}
fn grid_indices(n: usize) -> impl Iterator<Item = (usize, usize, usize, usize)> {
(0..n).flat_map(move |iz| {
(0..n).flat_map(move |iy| (0..n).map(move |ix| (ix, iy, iz, ix + iy * n + iz * n * n)))
})
}
fn check_volume_conservation(
before: &SdfNode,
after: &SdfNode,
relative_tolerance: f32,
law_name: &str,
priority: Priority,
config: &CheckConfig,
) -> Option<Violation> {
let mut before_count: usize = 0;
let mut after_count: usize = 0;
let mut sample_region: Option<(Vec3, Vec3Interval)> = None;
for (center, bounds) in GridSampler::new(config) {
let db = sdf_eval(before, center);
let da = sdf_eval(after, center);
if db < 0.0 {
before_count += 1;
}
if da < 0.0 {
after_count += 1;
if sample_region.is_none() {
sample_region = Some((center, bounds));
}
} else if db < 0.0 && sample_region.is_none() {
sample_region = Some((center, bounds));
}
}
if before_count == 0 {
return None; }
#[allow(clippy::cast_precision_loss)]
let diff = ((after_count as i64) - (before_count as i64)).unsigned_abs();
#[allow(clippy::cast_precision_loss)]
let relative_diff = (diff as f32) / (before_count as f32);
if relative_diff > relative_tolerance {
let (point, region) = sample_region.unwrap_or_else(|| {
(
(config.aabb_min + config.aabb_max) * 0.5,
Vec3Interval {
x: Interval {
lo: config.aabb_min.x,
hi: config.aabb_max.x,
},
y: Interval {
lo: config.aabb_min.y,
hi: config.aabb_max.y,
},
z: Interval {
lo: config.aabb_min.z,
hi: config.aabb_max.z,
},
},
)
});
Some(Violation {
law_name: law_name.to_string(),
priority,
residual: relative_tolerance - relative_diff, point,
region,
})
} else {
None
}
}
#[derive(Debug, Clone, Default)]
pub struct LawSet {
laws: Vec<Law>,
}
impl LawSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn hard(mut self, name: impl Into<String>, constraint: Constraint) -> Self {
self.laws.push(Law::hard(name, constraint));
self
}
#[must_use]
pub fn soft(mut self, name: impl Into<String>, weight: f32, constraint: Constraint) -> Self {
self.laws.push(Law::soft(name, weight, constraint));
self
}
#[must_use]
pub fn laws(&self) -> &[Law] {
&self.laws
}
#[must_use]
pub fn stress(
self,
name: impl Into<String>,
node: SdfNode,
load_points: Vec<(Vec3, f32)>,
min_thickness_factor: f32,
) -> Self {
self.hard(
name,
Constraint::Stress {
node,
load_points,
min_thickness_factor,
},
)
}
#[must_use]
pub fn thermal(
self,
name: impl Into<String>,
node: SdfNode,
heat_sources: Vec<Vec3>,
search_radius: f32,
min_surface_ratio: f32,
) -> Self {
self.hard(
name,
Constraint::Thermal {
node,
heat_sources,
search_radius,
min_surface_ratio,
},
)
}
#[must_use]
pub fn contact(
self,
name: impl Into<String>,
a: SdfNode,
b: SdfNode,
min_distance: f32,
max_distance: f32,
) -> Self {
self.hard(
name,
Constraint::Contact {
a,
b,
min_distance,
max_distance,
},
)
}
#[must_use]
pub fn continuity(self, name: impl Into<String>, node: SdfNode, seed_point: Vec3) -> Self {
self.hard(name, Constraint::Continuity { node, seed_point })
}
#[must_use]
pub fn volume_conservation(
self,
name: impl Into<String>,
before: SdfNode,
after: SdfNode,
relative_tolerance: f32,
) -> Self {
self.hard(
name,
Constraint::VolumeConservation {
before,
after,
relative_tolerance,
},
)
}
#[must_use]
pub fn check(&self, config: &CheckConfig) -> LawReport {
check_laws(&self.laws, config)
}
#[must_use]
pub fn detect_contradictions(&self) -> Vec<Contradiction> {
let mut contradictions = Vec::new();
for (i, law_i) in self.laws.iter().enumerate() {
for law_j in &self.laws[i + 1..] {
if let Some(reason) = check_contradiction(&law_i.constraint, &law_j.constraint) {
contradictions.push(Contradiction {
law_a: law_i.name.clone(),
law_b: law_j.name.clone(),
reason,
});
}
}
}
contradictions
}
}
#[derive(Debug, Clone)]
pub struct Contradiction {
pub law_a: String,
pub law_b: String,
pub reason: String,
}
fn check_contradiction(a: &Constraint, b: &Constraint) -> Option<String> {
match (a, b) {
(Constraint::NonOverlap { a: na, b: nb }, Constraint::Containment { inner, outer })
| (Constraint::Containment { inner, outer }, Constraint::NonOverlap { a: na, b: nb }) => {
let dbg_a = format!("{na:?}");
let dbg_b = format!("{nb:?}");
let dbg_inner = format!("{inner:?}");
let dbg_outer = format!("{outer:?}");
if (dbg_a == dbg_inner && dbg_b == dbg_outer)
|| (dbg_a == dbg_outer && dbg_b == dbg_inner)
{
Some(
"NonOverlap と Containment が同一ノードペアに適用: 内包されるならば必ず重なる"
.to_string(),
)
} else {
None
}
}
(
Constraint::NonOverlap { a: na, b: nb },
Constraint::Contact {
a: ca,
b: cb,
min_distance,
..
},
)
| (
Constraint::Contact {
a: ca,
b: cb,
min_distance,
..
},
Constraint::NonOverlap { a: na, b: nb },
) => {
let dbg_a = format!("{na:?}");
let dbg_b = format!("{nb:?}");
let dbg_ca = format!("{ca:?}");
let dbg_cb = format!("{cb:?}");
let same_pair =
(dbg_a == dbg_ca && dbg_b == dbg_cb) || (dbg_a == dbg_cb && dbg_b == dbg_ca);
if same_pair && *min_distance <= 0.0 {
Some(
"NonOverlap と Contact(min<=0) が同一ノードペアに適用: 接触を許容と禁止が同時"
.to_string(),
)
} else {
None
}
}
_ => None,
}
}
#[must_use]
pub fn top_violations(report: &LawReport, n: usize) -> Vec<&Violation> {
report.violations.iter().take(n).collect()
}
#[must_use]
pub fn hard_violations(report: &LawReport) -> Vec<&Violation> {
report
.violations
.iter()
.filter(|v| v.priority == Priority::Hard)
.collect()
}
#[must_use]
pub fn soft_violations(report: &LawReport) -> Vec<&Violation> {
report
.violations
.iter()
.filter(|v| matches!(v.priority, Priority::Soft(_)))
.collect()
}
#[must_use]
pub fn format_report(report: &LawReport) -> String {
use std::fmt::Write;
let mut out = String::new();
let _ = writeln!(
out,
"Law Check: {}/{} passed",
report.passed, report.total_laws
);
if report.all_passed() {
let _ = writeln!(out, " All laws satisfied.");
return out;
}
for v in &report.violations {
let severity = match v.priority {
Priority::Hard => "ERROR",
Priority::Soft(_) => "WARN ",
};
let _ = writeln!(
out,
" [{severity}] {}: residual={:.4}, at=({:.2},{:.2},{:.2}), region=[{:.2}..{:.2}]x[{:.2}..{:.2}]x[{:.2}..{:.2}]",
v.law_name,
v.residual,
v.point.x, v.point.y, v.point.z,
v.region.x.lo, v.region.x.hi,
v.region.y.lo, v.region.y.hi,
v.region.z.lo, v.region.z.hi,
);
}
out
}