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)]

//! VRML / WRL world format export.

/// A VRML shape node.
#[derive(Clone, Debug)]
pub struct WrlShape {
    pub positions: Vec<[f32; 3]>,
    pub indices: Vec<i32>,
    pub diffuse_color: [f32; 3],
}

/// A VRML world document.
#[derive(Clone, Debug, Default)]
pub struct WrlDocument {
    pub shapes: Vec<WrlShape>,
}

/// Create a new WRL document.
pub fn new_wrl_document() -> WrlDocument {
    WrlDocument::default()
}

/// Add a shape to the document.
pub fn wrl_add_shape(
    doc: &mut WrlDocument,
    positions: Vec<[f32; 3]>,
    indices: Vec<u32>,
    color: [f32; 3],
) {
    // VRML uses -1 to terminate faces
    let mut vrml_idx: Vec<i32> = Vec::new();
    for chunk in indices.chunks(3) {
        if chunk.len() == 3 {
            vrml_idx.push(chunk[0] as i32);
            vrml_idx.push(chunk[1] as i32);
            vrml_idx.push(chunk[2] as i32);
            vrml_idx.push(-1);
        }
    }
    doc.shapes.push(WrlShape {
        positions,
        indices: vrml_idx,
        diffuse_color: color,
    });
}

/// Return the shape count.
pub fn wrl_shape_count(doc: &WrlDocument) -> usize {
    doc.shapes.len()
}

/// Return the total vertex count across all shapes.
pub fn wrl_total_vertex_count(doc: &WrlDocument) -> usize {
    doc.shapes.iter().map(|s| s.positions.len()).sum()
}

/// Render the WRL file content.
pub fn render_wrl(doc: &WrlDocument) -> String {
    let mut out = String::from("#VRML V2.0 utf8\n# Generated by oxihuman\n");
    for shape in &doc.shapes {
        let [r, g, b] = shape.diffuse_color;
        let pts: Vec<String> = shape
            .positions
            .iter()
            .map(|p| format!("{:.4} {:.4} {:.4}", p[0], p[1], p[2]))
            .collect();
        let idx: Vec<String> = shape.indices.iter().map(|i| i.to_string()).collect();
        out.push_str("Shape {\n");
        out.push_str(&format!(
            "  appearance Appearance {{ material Material {{ diffuseColor {:.4} {:.4} {:.4} }} }}\n",
            r, g, b
        ));
        out.push_str("  geometry IndexedFaceSet {\n");
        out.push_str(&format!(
            "    coord Coordinate {{ point [ {} ] }}\n",
            pts.join(", ")
        ));
        out.push_str(&format!("    coordIndex [ {} ]\n", idx.join(", ")));
        out.push_str("  }\n}\n");
    }
    out
}

/// Estimate the file size.
pub fn wrl_size_estimate(doc: &WrlDocument) -> usize {
    render_wrl(doc).len()
}

/// Validate the document.
pub fn validate_wrl(doc: &WrlDocument) -> bool {
    doc.shapes.iter().all(|s| {
        !s.positions.is_empty() && s.diffuse_color.iter().all(|&c| (0.0..=1.0).contains(&c))
    })
}

/// Export mesh directly as a WRL document.
pub fn export_mesh_as_wrl(positions: &[[f32; 3]], indices: &[u32], color: [f32; 3]) -> WrlDocument {
    let mut doc = new_wrl_document();
    wrl_add_shape(&mut doc, positions.to_vec(), indices.to_vec(), color);
    doc
}

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

    fn simple_doc() -> WrlDocument {
        export_mesh_as_wrl(
            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
            &[0, 1, 2],
            [0.8, 0.5, 0.2],
        )
    }

    #[test]
    fn shape_count_one() {
        let d = simple_doc();
        assert_eq!(wrl_shape_count(&d), 1);
    }

    #[test]
    fn total_vertex_count() {
        let d = simple_doc();
        assert_eq!(wrl_total_vertex_count(&d), 3);
    }

    #[test]
    fn render_starts_with_vrml() {
        let d = simple_doc();
        assert!(render_wrl(&d).starts_with("#VRML V2.0"));
    }

    #[test]
    fn render_contains_shape() {
        let d = simple_doc();
        assert!(render_wrl(&d).contains("Shape {"));
    }

    #[test]
    fn render_contains_indexed_face_set() {
        let d = simple_doc();
        assert!(render_wrl(&d).contains("IndexedFaceSet"));
    }

    #[test]
    fn validate_valid_doc() {
        let d = simple_doc();
        assert!(validate_wrl(&d));
    }

    #[test]
    fn wrl_size_estimate_positive() {
        let d = simple_doc();
        assert!(wrl_size_estimate(&d) > 0);
    }

    #[test]
    fn indices_have_minus_one_terminator() {
        let d = simple_doc();
        let shape = &d.shapes[0];
        assert!(shape.indices.contains(&-1));
    }
}