use crate::scene::Vec3;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Material {
Air,
Drywall,
Brick,
ConcreteDry,
ReinforcedConcrete,
SheetSteel,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct LosSegment {
pub material: Material,
pub path_m: f64,
}
pub fn material_loss_db_per_m(m: Material) -> f64 {
match m {
Material::Air => 0.0,
Material::Drywall => 0.0, Material::Brick => 0.0, Material::ConcreteDry => 0.5, Material::ReinforcedConcrete => 20.0, Material::SheetSteel => 100.0, }
}
pub fn material_is_heavy(m: Material) -> bool {
matches!(m, Material::ReinforcedConcrete | Material::SheetSteel)
}
pub fn attenuate(b_in: Vec3, segments: &[LosSegment]) -> (Vec3, bool) {
let mut total_db = 0.0_f64;
let mut heavy = false;
for seg in segments {
if !seg.path_m.is_finite() || seg.path_m <= 0.0 {
continue;
}
total_db += seg.path_m * material_loss_db_per_m(seg.material);
heavy |= material_is_heavy(seg.material);
}
let scale = 10.0_f64.powf(-total_db / 20.0);
([b_in[0] * scale, b_in[1] * scale, b_in[2] * scale], heavy)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Propagator;
impl Propagator {
pub fn new() -> Self {
Self
}
pub fn attenuate(self, b_in: Vec3, segments: &[LosSegment]) -> (Vec3, bool) {
attenuate(b_in, segments)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn free_space_is_identity_transform() {
let b_in = [1.0e-9, 2.0e-9, 3.0e-9];
let segs = [LosSegment {
material: Material::Air,
path_m: 5.0,
}];
let (b_out, heavy) = attenuate(b_in, &segs);
assert_relative_eq!(b_out[0], b_in[0], max_relative = 1e-12);
assert_relative_eq!(b_out[1], b_in[1], max_relative = 1e-12);
assert_relative_eq!(b_out[2], b_in[2], max_relative = 1e-12);
assert!(!heavy);
}
#[test]
fn drywall_is_approximately_zero_db() {
let b_in = [1.0e-9, 0.0, 0.0];
let segs = [LosSegment {
material: Material::Drywall,
path_m: 0.1,
}];
let (b_out, heavy) = attenuate(b_in, &segs);
assert_relative_eq!(b_out[0], b_in[0], max_relative = 1e-12);
assert!(!heavy, "drywall is not flagged as heavy");
}
#[test]
fn dry_concrete_attenuates_at_half_db_per_meter() {
let b_in = [1.0_f64, 0.0, 0.0];
let segs = [LosSegment {
material: Material::ConcreteDry,
path_m: 2.0,
}];
let (b_out, heavy) = attenuate(b_in, &segs);
let expected = 10.0_f64.powf(-1.0 / 20.0);
assert_relative_eq!(b_out[0], expected, max_relative = 1e-12);
assert!(!heavy, "dry concrete is not flagged heavy");
}
#[test]
fn reinforced_concrete_attenuates_and_raises_heavy_flag() {
let b_in = [1.0_f64; 3];
let segs = [LosSegment {
material: Material::ReinforcedConcrete,
path_m: 0.2,
}];
let (b_out, heavy) = attenuate(b_in, &segs);
let expected = 10.0_f64.powf(-4.0 / 20.0);
for &val in &b_out {
assert_relative_eq!(val, expected, max_relative = 1e-12);
}
assert!(heavy, "reinforced concrete must raise heavy_flag");
}
#[test]
fn nan_or_negative_path_is_skipped_without_nan_in_output() {
let b_in = [1.0_f64, 2.0, 3.0];
let segs = [
LosSegment {
material: Material::ConcreteDry,
path_m: f64::NAN,
},
LosSegment {
material: Material::Drywall,
path_m: -1.0, },
LosSegment {
material: Material::Air,
path_m: 5.0,
},
];
let (b_out, heavy) = attenuate(b_in, &segs);
for k in 0..3 {
assert!(
b_out[k].is_finite(),
"B[{k}] = {} is non-finite — pass-3 NaN guard failed",
b_out[k]
);
assert_relative_eq!(b_out[k], b_in[k], max_relative = 1e-12);
}
assert!(!heavy);
}
#[test]
fn empty_los_returns_input_unchanged() {
let b_in = [1.0_f64, 2.0, 3.0];
let (b_out, heavy) = attenuate(b_in, &[]);
assert_eq!(b_out, b_in);
assert!(!heavy);
}
#[test]
fn propagator_struct_dispatches_to_free_function() {
let b_in = [1.0_f64, 2.0, 3.0];
let segs = [LosSegment {
material: Material::Air,
path_m: 1.0,
}];
let p = Propagator::new();
let (b_out, _) = p.attenuate(b_in, &segs);
assert_eq!(b_out, b_in);
}
}