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

//! G-code export for CNC/3D printing from mesh slice data.

/// A single G-code command line.
#[derive(Debug, Clone)]
pub struct GcodeCommand {
    pub line: String,
}

/// A G-code document (list of commands).
#[derive(Debug, Clone, Default)]
pub struct GcodeDocument {
    pub commands: Vec<GcodeCommand>,
    pub layer_count: usize,
}

impl GcodeDocument {
    pub fn new() -> Self {
        Self::default()
    }
}

/// Create a new G-code document with standard header.
pub fn new_gcode_document(feedrate_mm_min: f32) -> GcodeDocument {
    let mut doc = GcodeDocument::new();
    doc.commands.push(GcodeCommand { line: "; Generated by OxiHuman".into() });
    doc.commands.push(GcodeCommand { line: "G21 ; mm units".into() });
    doc.commands.push(GcodeCommand { line: "G90 ; absolute positioning".into() });
    doc.commands.push(GcodeCommand { line: format!("G1 F{:.1}", feedrate_mm_min) });
    doc
}

/// Add a move command (G1) to the document.
pub fn add_move(doc: &mut GcodeDocument, x: f32, y: f32, z: f32, extrude: f32) {
    let cmd = format!("G1 X{:.3} Y{:.3} Z{:.3} E{:.4}", x, y, z, extrude);
    doc.commands.push(GcodeCommand { line: cmd });
}

/// Add a rapid move (G0) command.
pub fn add_rapid(doc: &mut GcodeDocument, x: f32, y: f32, z: f32) {
    doc.commands.push(GcodeCommand { line: format!("G0 X{:.3} Y{:.3} Z{:.3}", x, y, z) });
}

/// Add a comment line.
pub fn add_comment(doc: &mut GcodeDocument, comment: &str) {
    doc.commands.push(GcodeCommand { line: format!("; {}", comment) });
}

/// Export a G-code document to a string.
pub fn export_gcode(doc: &GcodeDocument) -> String {
    let mut out = String::new();
    for cmd in &doc.commands {
        out.push_str(&cmd.line);
        out.push('\n');
    }
    out
}

/// Estimate print time in minutes given total travel distance and feedrate.
pub fn estimate_print_time_min(total_mm: f32, feedrate_mm_min: f32) -> f32 {
    if feedrate_mm_min <= 0.0 { return 0.0; }
    total_mm / feedrate_mm_min
}

/// Count move commands in a document.
pub fn move_command_count(doc: &GcodeDocument) -> usize {
    doc.commands.iter().filter(|c| c.line.starts_with("G1 X")).count()
}

/// Total command count.
pub fn command_count(doc: &GcodeDocument) -> usize {
    doc.commands.len()
}

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

    #[test]
    fn test_new_document_has_header() {
        /* new document has at least header commands */
        let doc = new_gcode_document(3000.0);
        assert!(command_count(&doc) >= 3);
    }

    #[test]
    fn test_add_move_increases_count() {
        /* adding move increases command count */
        let mut doc = new_gcode_document(1500.0);
        let before = command_count(&doc);
        add_move(&mut doc, 10.0, 20.0, 0.5, 1.0);
        assert_eq!(command_count(&doc), before + 1);
    }

    #[test]
    fn test_add_rapid_increases_count() {
        /* adding rapid move increases command count */
        let mut doc = new_gcode_document(1500.0);
        let before = command_count(&doc);
        add_rapid(&mut doc, 0.0, 0.0, 10.0);
        assert_eq!(command_count(&doc), before + 1);
    }

    #[test]
    fn test_export_gcode_contains_moves() {
        /* exported string contains G1 */
        let mut doc = new_gcode_document(1500.0);
        add_move(&mut doc, 5.0, 5.0, 0.0, 0.5);
        let s = export_gcode(&doc);
        assert!(s.contains("G1 X"));
    }

    #[test]
    fn test_move_command_count() {
        /* move_command_count counts correctly */
        let mut doc = new_gcode_document(1500.0);
        add_move(&mut doc, 1.0, 2.0, 0.0, 0.1);
        add_move(&mut doc, 3.0, 4.0, 0.0, 0.2);
        assert_eq!(move_command_count(&doc), 2);
    }

    #[test]
    fn test_add_comment() {
        /* comments appear in output */
        let mut doc = GcodeDocument::new();
        add_comment(&mut doc, "layer 1");
        let s = export_gcode(&doc);
        assert!(s.contains("; layer 1"));
    }

    #[test]
    fn test_estimate_print_time() {
        /* 600mm at 3000mm/min = 0.2 min */
        let t = estimate_print_time_min(600.0, 3000.0);
        assert!((t - 0.2).abs() < 1e-5);
    }

    #[test]
    fn test_estimate_print_time_zero_feedrate() {
        /* zero feedrate returns 0 */
        assert!((estimate_print_time_min(100.0, 0.0)).abs() < 1e-6);
    }

    #[test]
    fn test_export_ends_with_newline() {
        /* each command ends with newline */
        let doc = new_gcode_document(1200.0);
        let s = export_gcode(&doc);
        assert!(s.ends_with('\n'));
    }

    #[test]
    fn test_empty_document_export() {
        /* empty document exports empty string */
        let doc = GcodeDocument::new();
        assert_eq!(export_gcode(&doc), "");
    }
}