#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#[derive(Debug, Clone, PartialEq)]
pub struct LedPanelSpec {
pub width_px: u32,
pub height_px: u32,
pub pitch_mm: f32,
pub nits_max: u32,
pub refresh_hz: f32,
}
impl LedPanelSpec {
#[must_use]
pub fn new(
width_px: u32,
height_px: u32,
pitch_mm: f32,
nits_max: u32,
refresh_hz: f32,
) -> Self {
Self {
width_px,
height_px,
pitch_mm,
nits_max,
refresh_hz,
}
}
#[must_use]
pub fn physical_width_mm(&self) -> f32 {
self.width_px as f32 * self.pitch_mm
}
#[must_use]
pub fn physical_height_mm(&self) -> f32 {
self.height_px as f32 * self.pitch_mm
}
#[must_use]
pub fn pixel_count(&self) -> u64 {
u64::from(self.width_px) * u64::from(self.height_px)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LedWallSegment {
pub id: u32,
pub panels_wide: u32,
pub panels_high: u32,
pub spec: LedPanelSpec,
pub curved: bool,
pub curvature_deg: f32,
}
impl LedWallSegment {
#[must_use]
pub fn flat(id: u32, panels_wide: u32, panels_high: u32, spec: LedPanelSpec) -> Self {
Self {
id,
panels_wide,
panels_high,
spec,
curved: false,
curvature_deg: 0.0,
}
}
#[must_use]
pub fn curved(
id: u32,
panels_wide: u32,
panels_high: u32,
spec: LedPanelSpec,
curvature_deg: f32,
) -> Self {
Self {
id,
panels_wide,
panels_high,
spec,
curved: true,
curvature_deg,
}
}
#[must_use]
pub fn total_width_mm(&self) -> f32 {
self.panels_wide as f32 * self.spec.physical_width_mm()
}
#[must_use]
pub fn total_height_mm(&self) -> f32 {
self.panels_high as f32 * self.spec.physical_height_mm()
}
#[must_use]
pub fn total_pixels(&self) -> u64 {
u64::from(self.panels_wide) * u64::from(self.panels_high) * self.spec.pixel_count()
}
}
#[derive(Debug, Clone)]
pub struct LedVolume {
pub segments: Vec<LedWallSegment>,
pub ceiling: Option<LedWallSegment>,
pub floor: Option<LedWallSegment>,
}
impl LedVolume {
#[must_use]
pub fn new() -> Self {
Self {
segments: Vec::new(),
ceiling: None,
floor: None,
}
}
pub fn add_segment(&mut self, seg: LedWallSegment) {
self.segments.push(seg);
}
#[must_use]
pub fn total_nits_max(&self) -> u32 {
let mut max_nits = 0u32;
for seg in &self.segments {
if seg.spec.nits_max > max_nits {
max_nits = seg.spec.nits_max;
}
}
if let Some(c) = &self.ceiling {
if c.spec.nits_max > max_nits {
max_nits = c.spec.nits_max;
}
}
if let Some(f) = &self.floor {
if f.spec.nits_max > max_nits {
max_nits = f.spec.nits_max;
}
}
max_nits
}
#[must_use]
pub fn total_pixels(&self) -> u64 {
let mut total: u64 = self.segments.iter().map(LedWallSegment::total_pixels).sum();
if let Some(c) = &self.ceiling {
total += c.total_pixels();
}
if let Some(f) = &self.floor {
total += f.total_pixels();
}
total
}
#[must_use]
pub fn has_ceiling(&self) -> bool {
self.ceiling.is_some()
}
}
impl Default for LedVolume {
fn default() -> Self {
Self::new()
}
}
pub struct MoireRiskAssessor;
impl MoireRiskAssessor {
#[must_use]
pub fn assess(panel: &LedPanelSpec, camera_distance_m: f64, lens_focal_mm: f32) -> f32 {
if camera_distance_m <= 0.0 || lens_focal_mm <= 0.0 {
return 1.0; }
let pitch_m = f64::from(panel.pitch_mm) / 1_000.0;
let apparent_mm = (pitch_m / camera_distance_m) * f64::from(lens_focal_mm);
let sensor_pixel_pitch_mm = 0.006_f64;
let ratio = apparent_mm / sensor_pixel_pitch_mm;
let frac = ratio - ratio.floor();
let risk = 1.0 - (2.0 * (frac - 0.5).abs());
risk.clamp(0.0, 1.0) as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_panel() -> LedPanelSpec {
LedPanelSpec::new(256, 128, 2.8, 1500, 3840.0)
}
#[test]
fn test_panel_physical_width() {
let p = LedPanelSpec::new(256, 128, 2.8, 1500, 3840.0);
let expected = 256.0_f32 * 2.8_f32;
assert!((p.physical_width_mm() - expected).abs() < 1e-3);
}
#[test]
fn test_panel_physical_height() {
let p = LedPanelSpec::new(256, 128, 2.8, 1500, 3840.0);
let expected = 128.0_f32 * 2.8_f32;
assert!((p.physical_height_mm() - expected).abs() < 1e-3);
}
#[test]
fn test_panel_pixel_count() {
let p = LedPanelSpec::new(256, 128, 2.8, 1500, 3840.0);
assert_eq!(p.pixel_count(), 256 * 128);
}
#[test]
fn test_segment_flat_creation() {
let seg = LedWallSegment::flat(1, 4, 2, sample_panel());
assert!(!seg.curved);
assert_eq!(seg.curvature_deg, 0.0);
}
#[test]
fn test_segment_curved_creation() {
let seg = LedWallSegment::curved(2, 6, 3, sample_panel(), 180.0);
assert!(seg.curved);
assert!((seg.curvature_deg - 180.0).abs() < 1e-5);
}
#[test]
fn test_segment_total_width() {
let spec = LedPanelSpec::new(256, 128, 2.8, 1500, 3840.0);
let seg = LedWallSegment::flat(1, 4, 2, spec);
let expected = 4.0_f32 * 256.0_f32 * 2.8_f32;
assert!((seg.total_width_mm() - expected).abs() < 1e-2);
}
#[test]
fn test_segment_total_height() {
let spec = LedPanelSpec::new(256, 128, 2.8, 1500, 3840.0);
let seg = LedWallSegment::flat(1, 4, 2, spec);
let expected = 2.0_f32 * 128.0_f32 * 2.8_f32;
assert!((seg.total_height_mm() - expected).abs() < 1e-2);
}
#[test]
fn test_segment_total_pixels() {
let spec = LedPanelSpec::new(256, 128, 2.8, 1500, 3840.0);
let seg = LedWallSegment::flat(1, 4, 2, spec);
assert_eq!(seg.total_pixels(), 4 * 2 * 256 * 128);
}
#[test]
fn test_volume_add_segment() {
let mut vol = LedVolume::new();
vol.add_segment(LedWallSegment::flat(1, 4, 2, sample_panel()));
assert_eq!(vol.segments.len(), 1);
}
#[test]
fn test_volume_total_nits_max_empty() {
let vol = LedVolume::new();
assert_eq!(vol.total_nits_max(), 0);
}
#[test]
fn test_volume_total_nits_max_segments() {
let mut vol = LedVolume::new();
let spec_hi = LedPanelSpec::new(256, 128, 2.8, 2000, 3840.0);
vol.add_segment(LedWallSegment::flat(1, 4, 2, sample_panel())); vol.add_segment(LedWallSegment::flat(2, 4, 2, spec_hi)); assert_eq!(vol.total_nits_max(), 2000);
}
#[test]
fn test_volume_has_ceiling_false() {
let vol = LedVolume::new();
assert!(!vol.has_ceiling());
}
#[test]
fn test_volume_has_ceiling_true() {
let mut vol = LedVolume::new();
vol.ceiling = Some(LedWallSegment::flat(99, 4, 2, sample_panel()));
assert!(vol.has_ceiling());
}
#[test]
fn test_volume_total_pixels_with_ceiling() {
let mut vol = LedVolume::new();
let seg = LedWallSegment::flat(1, 1, 1, LedPanelSpec::new(10, 10, 1.0, 1000, 60.0));
let ceil = LedWallSegment::flat(2, 1, 1, LedPanelSpec::new(10, 10, 1.0, 1000, 60.0));
vol.add_segment(seg);
vol.ceiling = Some(ceil);
assert_eq!(vol.total_pixels(), 200);
}
#[test]
fn test_moire_risk_assessor_range() {
let panel = sample_panel();
let risk = MoireRiskAssessor::assess(&panel, 5.0, 50.0);
assert!((0.0..=1.0).contains(&risk));
}
#[test]
fn test_moire_risk_assessor_degenerate() {
let panel = sample_panel();
let risk = MoireRiskAssessor::assess(&panel, 0.0, 50.0);
assert_eq!(risk, 1.0);
}
}