oxihuman-export 0.1.2

Export pipeline for OxiHuman — glTF, COLLADA, STL, and streaming formats
Documentation
// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
// SPDX-License-Identifier: Apache-2.0
#![allow(dead_code)]

//! Neutral File Format (NFF) export.
//! NFF is a simple scene description language used by Eric Haines' ray tracers.

/// An NFF point light source.
#[derive(Clone, Debug)]
pub struct NffLight {
    pub position: [f32; 3],
    pub color: [f32; 3],
}

/// NFF material surface properties.
#[derive(Clone, Debug)]
pub struct NffSurface {
    pub color: [f32; 3],
    pub kd: f32,
    pub ks: f32,
    pub shine: f32,
    pub transmittance: f32,
    pub ior: f32,
}

impl Default for NffSurface {
    fn default() -> Self {
        Self {
            color: [0.8, 0.8, 0.8],
            kd: 1.0,
            ks: 0.0,
            shine: 0.0,
            transmittance: 0.0,
            ior: 1.0,
        }
    }
}

/// An NFF polygon (flat, convex).
#[derive(Clone, Debug)]
pub struct NffPolygon {
    pub vertices: Vec<[f32; 3]>,
    pub surface: NffSurface,
}

/// An NFF sphere primitive.
#[derive(Clone, Debug)]
pub struct NffSphere {
    pub center: [f32; 3],
    pub radius: f32,
    pub surface: NffSurface,
}

/// An NFF scene document.
#[derive(Clone, Debug, Default)]
pub struct NffDocument {
    pub background: [f32; 3],
    pub lights: Vec<NffLight>,
    pub polygons: Vec<NffPolygon>,
    pub spheres: Vec<NffSphere>,
}

/// Create a new NFF document with a white background.
pub fn new_nff_document() -> NffDocument {
    NffDocument {
        background: [1.0, 1.0, 1.0],
        ..Default::default()
    }
}

/// Set the background color.
pub fn nff_set_background(doc: &mut NffDocument, color: [f32; 3]) {
    doc.background = color;
}

/// Add a point light.
pub fn nff_add_light(doc: &mut NffDocument, position: [f32; 3], color: [f32; 3]) {
    doc.lights.push(NffLight { position, color });
}

/// Add a polygon.
pub fn nff_add_polygon(doc: &mut NffDocument, vertices: Vec<[f32; 3]>, surface: NffSurface) {
    doc.polygons.push(NffPolygon { vertices, surface });
}

/// Add a sphere.
pub fn nff_add_sphere(doc: &mut NffDocument, center: [f32; 3], radius: f32, surface: NffSurface) {
    doc.spheres.push(NffSphere {
        center,
        radius,
        surface,
    });
}

/// Add a triangle mesh as polygons.
pub fn nff_add_mesh(
    doc: &mut NffDocument,
    positions: &[[f32; 3]],
    indices: &[u32],
    surface: NffSurface,
) {
    for tri in indices.chunks(3) {
        if tri.len() == 3 {
            let verts = vec![
                positions[tri[0] as usize],
                positions[tri[1] as usize],
                positions[tri[2] as usize],
            ];
            doc.polygons.push(NffPolygon {
                vertices: verts,
                surface: surface.clone(),
            });
        }
    }
}

/// Return the light count.
pub fn nff_light_count(doc: &NffDocument) -> usize {
    doc.lights.len()
}

/// Return the polygon count.
pub fn nff_polygon_count(doc: &NffDocument) -> usize {
    doc.polygons.len()
}

/// Return the sphere count.
pub fn nff_sphere_count(doc: &NffDocument) -> usize {
    doc.spheres.len()
}

/// Return the total primitive count (polygons + spheres).
pub fn nff_primitive_count(doc: &NffDocument) -> usize {
    doc.polygons.len() + doc.spheres.len()
}

fn render_nff_surface(s: &NffSurface) -> String {
    format!(
        "f {:.4} {:.4} {:.4}  {:.4} {:.4} {:.4} {:.4} {:.4}\n",
        s.color[0], s.color[1], s.color[2], s.kd, s.ks, s.shine, s.transmittance, s.ior
    )
}

/// Render the NFF scene to a string.
pub fn render_nff(doc: &NffDocument) -> String {
    let mut out = String::from("# NFF scene — generated by oxihuman\n");
    /* Background color */
    out.push_str(&format!(
        "b {:.4} {:.4} {:.4}\n",
        doc.background[0], doc.background[1], doc.background[2]
    ));
    /* Lights */
    for light in &doc.lights {
        let [lx, ly, lz] = light.position;
        let [lr, lg, lb] = light.color;
        out.push_str(&format!(
            "l {lx:.4} {ly:.4} {lz:.4} {lr:.4} {lg:.4} {lb:.4}\n"
        ));
    }
    /* Spheres */
    for sph in &doc.spheres {
        out.push_str(&render_nff_surface(&sph.surface));
        let [cx, cy, cz] = sph.center;
        out.push_str(&format!("s {cx:.4} {cy:.4} {cz:.4} {:.4}\n", sph.radius));
    }
    /* Polygons */
    for poly in &doc.polygons {
        out.push_str(&render_nff_surface(&poly.surface));
        out.push_str(&format!("p {}\n", poly.vertices.len()));
        for v in &poly.vertices {
            out.push_str(&format!("{:.6} {:.6} {:.6}\n", v[0], v[1], v[2]));
        }
    }
    out
}

/// Estimate the file size.
pub fn nff_size_estimate(doc: &NffDocument) -> usize {
    render_nff(doc).len()
}

/// Validate the document (all polygons >= 3 vertices, sphere radii > 0).
pub fn validate_nff(doc: &NffDocument) -> bool {
    let polys_ok = doc.polygons.iter().all(|p| p.vertices.len() >= 3);
    let spheres_ok = doc.spheres.iter().all(|s| s.radius > 0.0);
    polys_ok && spheres_ok
}

#[cfg(test)]
mod tests {
    use super::*;

    fn simple_doc() -> NffDocument {
        let mut doc = new_nff_document();
        nff_add_light(&mut doc, [1.0, 2.0, 3.0], [1.0, 1.0, 1.0]);
        nff_add_polygon(
            &mut doc,
            vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
            NffSurface::default(),
        );
        nff_add_sphere(&mut doc, [0.0, 0.0, 0.0], 1.0, NffSurface::default());
        doc
    }

    #[test]
    fn light_count() {
        assert_eq!(nff_light_count(&simple_doc()), 1);
    }

    #[test]
    fn polygon_count() {
        assert_eq!(nff_polygon_count(&simple_doc()), 1);
    }

    #[test]
    fn sphere_count() {
        assert_eq!(nff_sphere_count(&simple_doc()), 1);
    }

    #[test]
    fn primitive_count() {
        assert_eq!(nff_primitive_count(&simple_doc()), 2);
    }

    #[test]
    fn render_starts_with_comment() {
        let s = render_nff(&simple_doc());
        assert!(s.starts_with("# NFF"));
    }

    #[test]
    fn render_contains_background() {
        let s = render_nff(&simple_doc());
        assert!(s.contains("\nb "));
    }

    #[test]
    fn render_contains_polygon_marker() {
        let s = render_nff(&simple_doc());
        assert!(s.contains("\np 3"));
    }

    #[test]
    fn render_contains_sphere_marker() {
        let s = render_nff(&simple_doc());
        assert!(s.contains("\ns "));
    }

    #[test]
    fn validate_valid_doc() {
        assert!(validate_nff(&simple_doc()));
    }

    #[test]
    fn add_mesh_creates_polygons() {
        let mut doc = new_nff_document();
        nff_add_mesh(
            &mut doc,
            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
            &[0, 1, 2],
            NffSurface::default(),
        );
        assert_eq!(nff_polygon_count(&doc), 1);
    }

    #[test]
    fn size_estimate_positive() {
        assert!(nff_size_estimate(&simple_doc()) > 0);
    }
}