use crate::material::NUM_BANDS;
use crate::propagation::speed_of_sound;
use hisab::Vec3;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Portal {
pub position: Vec3,
pub normal: Vec3,
pub width: f32,
pub height: f32,
}
impl Portal {
#[must_use]
#[inline]
pub fn area(&self) -> f32 {
self.width * self.height
}
#[must_use]
pub fn transmission_factor(&self, temperature_celsius: f32) -> [f32; NUM_BANDS] {
let c = speed_of_sound(temperature_celsius);
let characteristic_size = (self.width * self.height).sqrt();
if characteristic_size <= 0.0 {
return [0.0; NUM_BANDS];
}
std::array::from_fn(|band| {
let freq = crate::material::FREQUENCY_BANDS[band];
let wavelength = c / freq;
let ratio = characteristic_size / wavelength;
(ratio / (1.0 + ratio)).clamp(0.0, 1.0)
})
}
}
#[must_use]
pub fn portal_energy_transfer(
source: Vec3,
portal: &Portal,
listener: Vec3,
temperature_celsius: f32,
) -> [f32; NUM_BANDS] {
let to_portal = portal.position - source;
let from_portal = listener - portal.position;
let d_source = to_portal.length();
let d_listener = from_portal.length();
if d_source < f32::EPSILON || d_listener < f32::EPSILON {
return [0.0; NUM_BANDS];
}
let cos_in = (to_portal / d_source).dot(portal.normal).abs();
let cos_out = (from_portal / d_listener).dot(portal.normal).abs();
let directional = cos_in * cos_out;
let pi4 = 4.0 * std::f32::consts::PI;
let distance_atten = portal.area() / (pi4 * d_source * d_source * d_listener * d_listener);
let distance_atten = distance_atten.min(1.0);
let freq_factors = portal.transmission_factor(temperature_celsius);
std::array::from_fn(|band| (freq_factors[band] * directional * distance_atten).clamp(0.0, 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
fn standard_door() -> Portal {
Portal {
position: Vec3::new(5.0, 1.0, 0.0),
normal: Vec3::Z,
width: 0.9,
height: 2.1,
}
}
#[test]
fn door_area() {
let door = standard_door();
assert!((door.area() - 1.89).abs() < 0.01);
}
#[test]
fn transmission_factor_high_freq_near_one() {
let door = standard_door();
let factors = door.transmission_factor(20.0);
assert!(
factors[7] > 0.9,
"8 kHz through door should be near 1.0, got {}",
factors[7]
);
}
#[test]
fn transmission_factor_low_freq_lower() {
let door = standard_door();
let factors = door.transmission_factor(20.0);
assert!(
factors[0] < factors[7],
"low freq ({}) should transmit less than high ({})",
factors[0],
factors[7]
);
}
#[test]
fn portal_energy_on_axis() {
let door = standard_door();
let source = Vec3::new(5.0, 1.0, -3.0);
let listener = Vec3::new(5.0, 1.0, 3.0);
let energy = portal_energy_transfer(source, &door, listener, 20.0);
for &e in &energy {
assert!(
(0.0..=1.0).contains(&e),
"energy should be in [0,1], got {e}"
);
}
assert!(energy[4] > 0.0, "1 kHz on-axis should have energy transfer");
}
#[test]
fn portal_energy_off_axis_lower() {
let door = standard_door();
let on_axis = portal_energy_transfer(
Vec3::new(5.0, 1.0, -3.0),
&door,
Vec3::new(5.0, 1.0, 3.0),
20.0,
);
let off_axis = portal_energy_transfer(
Vec3::new(0.0, 1.0, -3.0), &door,
Vec3::new(5.0, 1.0, 3.0),
20.0,
);
let on_sum: f32 = on_axis.iter().sum();
let off_sum: f32 = off_axis.iter().sum();
assert!(
on_sum > off_sum,
"on-axis ({on_sum}) should have more energy than off-axis ({off_sum})"
);
}
#[test]
fn zero_size_portal_no_transmission() {
let tiny = Portal {
position: Vec3::ZERO,
normal: Vec3::Z,
width: 0.0,
height: 0.0,
};
let factors = tiny.transmission_factor(20.0);
for &f in &factors {
assert_eq!(f, 0.0);
}
}
#[test]
fn portal_serializes() {
let door = standard_door();
let json = serde_json::to_string(&door).unwrap();
let back: Portal = serde_json::from_str(&json).unwrap();
assert_eq!(door, back);
}
}