autofoam 0.1.0

autofoam and related tools
use std::{
    env,
    fs::File,
    io::{BufRead, BufReader, Read, Seek, SeekFrom},
};

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();

    if args.is_empty() {
        eprintln!(
            "Usage: {} <file.stl> [file2.stl ...]",
            env::args().next().unwrap()
        );
        std::process::exit(1);
    }

    let mut min = [f32::INFINITY; 3];
    let mut max = [f32::NEG_INFINITY; 3];
    let mut vertex_count = 0;

    for path in &args {
        let mut file = File::open(path).unwrap_or_else(|e| {
            eprintln!("Failed to open {}: {}", path, e);
            std::process::exit(1);
        });

        let count = if is_ascii(&mut file) {
            process_ascii(file, &mut min, &mut max)
        } else {
            process_binary(file, &mut min, &mut max)
        }
        .unwrap_or_else(|e| {
            eprintln!("Failed to process {}: {}", path, e);
            std::process::exit(1);
        });

        vertex_count += count;
    }

    if vertex_count == 0 {
        eprintln!("No vertices found");
        std::process::exit(1);
    }

    println!(
        "{:.6} {:.6} {:.6} {:.6} {:.6} {:.6}",
        min[0], min[1], min[2], max[0], max[1], max[2]
    );
}

fn is_ascii(file: &mut File) -> bool {
    let mut header = [0u8; 5];
    file.read_exact(&mut header).expect("File too short");
    file.rewind().expect("Seek failed");
    header.starts_with(b"solid")
}

fn process_ascii(
    file: File,
    min: &mut [f32; 3],
    max: &mut [f32; 3],
) -> Result<usize, std::io::Error> {
    let mut count = 0;
    for line in BufReader::new(file).lines() {
        let line = line?;
        if let Some(coords) = line.trim().strip_prefix("vertex ") {
            let parts: Vec<&str> = coords.split_whitespace().collect();
            if parts.len() >= 3 {
                let x = parts[0].parse::<f32>().expect("Invalid vertex coordinate");
                let y = parts[1].parse::<f32>().expect("Invalid vertex coordinate");
                let z = parts[2].parse::<f32>().expect("Invalid vertex coordinate");
                update_bounds([x, y, z], min, max);
                count += 1;
            }
        }
    }
    Ok(count)
}

fn process_binary(
    mut file: File,
    min: &mut [f32; 3],
    max: &mut [f32; 3],
) -> Result<usize, std::io::Error> {
    file.seek(SeekFrom::Start(80))?;

    let mut buf = [0u8; 4];
    file.read_exact(&mut buf)?;
    let triangle_count = u32::from_le_bytes(buf) as usize;

    for _ in 0..triangle_count {
        file.seek(SeekFrom::Current(12))?; // Skip normal

        for _ in 0..3 {
            file.read_exact(&mut buf)?;
            let x = f32::from_le_bytes(buf);
            file.read_exact(&mut buf)?;
            let y = f32::from_le_bytes(buf);
            file.read_exact(&mut buf)?;
            let z = f32::from_le_bytes(buf);

            update_bounds([x, y, z], min, max);
        }

        file.seek(SeekFrom::Current(2))?; // Skip attributes
    }

    Ok(triangle_count * 3)
}

fn update_bounds(vertex: [f32; 3], min: &mut [f32; 3], max: &mut [f32; 3]) {
    for i in 0..3 {
        if vertex[i] < min[i] {
            min[i] = vertex[i];
        }
        if vertex[i] > max[i] {
            max[i] = vertex[i];
        }
    }
}