atanor 0.1.0

Motor 3D ray-traced que vive solo y exclusivamente en la terminal.
Documentation
use atanor::render::Camera;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::Frame;

pub struct HudStats {
    pub fps: f32,
    pub frame_ms: f32,
    pub rays: u32,
    pub bounces: u32,
    pub spp: u32,
}

pub fn render_overlay(f: &mut Frame, area: Rect, stats: &HudStats, cam: &Camera, paused: bool) {
    // Esquina superior izquierda: stats.
    let stats_area = Rect {
        x: area.x + 1,
        y: area.y,
        width: 32.min(area.width.saturating_sub(2)),
        height: 7.min(area.height),
    };
    let stats_lines = vec![
        Line::from(vec![
            Span::styled("atanor", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
            Span::raw("  motor 3d / terminal"),
        ]),
        Line::from(format!("fps   {:6.1}  ({:5.1} ms)", stats.fps, stats.frame_ms)),
        Line::from(format!("rays  {:>7}  /{}b", stats.rays, stats.bounces)),
        Line::from(format!("spp   {:>2}x   (Halton 2,3)", stats.spp)),
        Line::from(format!(
            "cam   {:+.1},{:+.1},{:+.1}",
            cam.position.x, cam.position.y, cam.position.z
        )),
    ];
    let stats_widget = Paragraph::new(stats_lines)
        .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::DarkGray)))
        .style(Style::default().fg(Color::Gray));
    f.render_widget(stats_widget, stats_area);

    // Esquina inferior: ayuda controles.
    let help_height = 3u16;
    if area.height > help_height + 1 {
        let help_area = Rect {
            x: area.x,
            y: area.y + area.height - help_height,
            width: area.width,
            height: help_height,
        };
        let help = Paragraph::new(vec![
            Line::from(vec![
                Span::styled("[WASD/Q/E]", Style::default().fg(Color::Cyan)),
                Span::raw(" mover  "),
                Span::styled("[← → ↑ ↓]", Style::default().fg(Color::Cyan)),
                Span::raw(" mirar  "),
                Span::styled("[Shift]", Style::default().fg(Color::Cyan)),
                Span::raw(" sprint  "),
                Span::styled("[ [ / ] ]", Style::default().fg(Color::Cyan)),
                Span::raw(" spp  "),
                Span::styled("[Espacio]", Style::default().fg(Color::Cyan)),
                Span::raw(" pausa  "),
                Span::styled("[Esc]", Style::default().fg(Color::Cyan)),
                Span::raw(" salir"),
            ]),
        ])
        .alignment(Alignment::Center)
        .wrap(Wrap { trim: true })
        .block(Block::default().borders(Borders::TOP).border_style(Style::default().fg(Color::DarkGray)));
        f.render_widget(help, help_area);
    }

    if paused {
        let msg = "  PAUSADO  ";
        let w = msg.len() as u16 + 2;
        let h = 3u16;
        let pause_area = Rect {
            x: area.x + area.width.saturating_sub(w) / 2,
            y: area.y + area.height.saturating_sub(h) / 2,
            width: w.min(area.width),
            height: h.min(area.height),
        };
        let widget = Paragraph::new(msg)
            .alignment(Alignment::Center)
            .style(Style::default().fg(Color::Black).bg(Color::Yellow).add_modifier(Modifier::BOLD))
            .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::Yellow)));
        f.render_widget(widget, pause_area);
    }
}