zoa 0.1.0

3D ASCII renderer for terminals with shapes, particles, and effects
Documentation
use std::ops::{Add, Mul, Sub};

#[derive(Debug, Clone, Copy, Default)]
pub struct Vec3 {
    pub x: f32,
    pub y: f32,
    pub z: f32,
}

impl Vec3 {
    pub const fn new(x: f32, y: f32, z: f32) -> Self {
        Self { x, y, z }
    }

    pub fn dot(self, other: Vec3) -> f32 {
        self.x * other.x + self.y * other.y + self.z * other.z
    }

    pub fn normalize(self) -> Self {
        let len = (self.x * self.x + self.y * self.y + self.z * self.z).sqrt();
        if len > 0.0 {
            Self {
                x: self.x / len,
                y: self.y / len,
                z: self.z / len,
            }
        } else {
            self
        }
    }

    pub fn rotate_x(self, angle: f32) -> Self {
        let (sin, cos) = angle.sin_cos();
        Self {
            x: self.x,
            y: self.y * cos - self.z * sin,
            z: self.y * sin + self.z * cos,
        }
    }

    pub fn rotate_y(self, angle: f32) -> Self {
        let (sin, cos) = angle.sin_cos();
        Self {
            x: self.x * cos + self.z * sin,
            y: self.y,
            z: -self.x * sin + self.z * cos,
        }
    }

    pub fn rotate_z(self, angle: f32) -> Self {
        let (sin, cos) = angle.sin_cos();
        Self {
            x: self.x * cos - self.y * sin,
            y: self.x * sin + self.y * cos,
            z: self.z,
        }
    }
}

impl Add for Vec3 {
    type Output = Self;
    fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
        }
    }
}

impl Sub for Vec3 {
    type Output = Self;
    fn sub(self, other: Self) -> Self {
        Self {
            x: self.x - other.x,
            y: self.y - other.y,
            z: self.z - other.z,
        }
    }
}

impl Mul<f32> for Vec3 {
    type Output = Self;
    fn mul(self, scalar: f32) -> Self {
        Self {
            x: self.x * scalar,
            y: self.y * scalar,
            z: self.z * scalar,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Camera {
    pub distance: f32,
    pub scale: f32,
}

impl Default for Camera {
    fn default() -> Self {
        Self {
            distance: 5.0,
            scale: 40.0,
        }
    }
}

impl Camera {
    pub fn project(&self, point: Vec3, screen_width: u16, screen_height: u16) -> Option<(u16, u16, f32)> {
        // Guard against zero-size buffers
        if screen_width == 0 || screen_height == 0 {
            return None;
        }

        let z = self.distance + point.z;
        if z <= 0.1 {
            return None;
        }

        let inv_z = 1.0 / z;
        let aspect = screen_width as f32 / screen_height as f32 * 0.5;

        let screen_x = (screen_width as f32 / 2.0 + point.x * self.scale * inv_z * aspect) as i32;
        let screen_y = (screen_height as f32 / 2.0 - point.y * self.scale * inv_z) as i32;

        if screen_x >= 0 && screen_x < screen_width as i32 && screen_y >= 0 && screen_y < screen_height as i32 {
            Some((screen_x as u16, screen_y as u16, inv_z))
        } else {
            None
        }
    }
}