oxihuman-export 0.1.2

Export pipeline for OxiHuman — glTF, COLLADA, STL, and streaming formats
Documentation
//! IGES (Initial Graphics Exchange Specification) CAD format export stub.
//!
//! Generates minimal IGES ASCII sections for CAD interchange.

use std::fmt::Write as FmtWrite;

// ── Enums ────────────────────────────────────────────────────────────────────

/// IGES file section identifier.
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum IgesSection {
    Start,
    Global,
    Directory,
    Parameter,
    Terminate,
}

// ── Structs ──────────────────────────────────────────────────────────────────

/// Configuration for IGES export.
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct IgesExportConfig {
    /// Author name embedded in the IGES header.
    pub author: String,
    /// Organisation name embedded in the IGES header.
    pub organisation: String,
    /// Originating system identifier.
    pub originating_system: String,
    /// Description field.
    pub description: String,
    /// IGES version string.
    pub version: String,
}

/// A single IGES entity (e.g. a point, line, curve).
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct IgesEntity {
    /// Entity sequence number (1-based).
    pub seq: u32,
    /// IGES entity type code (e.g. 116 = Point, 110 = Line).
    pub entity_type: u16,
    /// Parameter data as a flat string (comma-separated).
    pub param_data: String,
}

/// In-memory IGES document.
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct IgesDocument {
    /// Export configuration.
    pub config: IgesExportConfig,
    /// Ordered list of entities.
    pub entities: Vec<IgesEntity>,
    /// Counter for the next sequence number.
    next_seq: u32,
}

// ── Public functions ──────────────────────────────────────────────────────────

/// Returns a sensible default [`IgesExportConfig`].
#[allow(dead_code)]
pub fn default_iges_config() -> IgesExportConfig {
    IgesExportConfig {
        author: "OxiHuman".to_string(),
        organisation: "COOLJAPAN OU".to_string(),
        originating_system: "OxiHuman IGES Exporter 1.0".to_string(),
        description: "IGES export generated by OxiHuman".to_string(),
        version: "5.3".to_string(),
    }
}

/// Creates a new, empty [`IgesDocument`] with the given config.
#[allow(dead_code)]
pub fn new_iges_document(cfg: &IgesExportConfig) -> IgesDocument {
    IgesDocument {
        config: cfg.clone(),
        entities: Vec::new(),
        next_seq: 1,
    }
}

/// Appends a point entity (type 116) and returns the assigned sequence number.
#[allow(dead_code)]
pub fn iges_add_point(doc: &mut IgesDocument, x: f64, y: f64, z: f64) -> u32 {
    let seq = doc.next_seq;
    doc.next_seq += 1;
    doc.entities.push(IgesEntity {
        seq,
        entity_type: 116,
        param_data: format!("{x:.6},{y:.6},{z:.6}"),
    });
    seq
}

/// Appends a line entity (type 110) and returns the assigned sequence number.
#[allow(dead_code)]
pub fn iges_add_line(
    doc: &mut IgesDocument,
    x1: f64,
    y1: f64,
    z1: f64,
    x2: f64,
    y2: f64,
    z2: f64,
) -> u32 {
    let seq = doc.next_seq;
    doc.next_seq += 1;
    doc.entities.push(IgesEntity {
        seq,
        entity_type: 110,
        param_data: format!("{x1:.6},{y1:.6},{z1:.6},{x2:.6},{y2:.6},{z2:.6}"),
    });
    seq
}

/// Serialises the document to an IGES ASCII string.
#[allow(dead_code)]
pub fn iges_to_string(doc: &IgesDocument) -> String {
    let mut out = String::new();
    out.push_str(&iges_header_string(doc));
    // Global section
    let _ = writeln!(
        out,
        "{}",
        iges_section_line(&format!("{}G      1", doc.config.originating_system), 'G', 1)
    );
    // Directory and parameter sections
    for ent in &doc.entities {
        let dir_line = format!("{:8}       0       0       0       0", ent.entity_type);
        let _ = writeln!(out, "{}D{:7}", dir_line, ent.seq);
        let param_line = format!("{},{};", ent.entity_type, ent.param_data);
        let _ = writeln!(out, "{}P{:7}", param_line, ent.seq);
    }
    // Terminate
    let _ = writeln!(
        out,
        "S      1G      1D{:6}P{:6}                                        T      1",
        doc.entities.len(),
        doc.entities.len()
    );
    out
}

/// Writes the IGES document to a file at `path`.
#[allow(dead_code)]
pub fn iges_write_to_file(doc: &IgesDocument, path: &str) -> Result<(), String> {
    let content = iges_to_string(doc);
    std::fs::write(path, content).map_err(|e| e.to_string())
}

/// Returns the number of entities in the document.
#[allow(dead_code)]
pub fn iges_entity_count(doc: &IgesDocument) -> usize {
    doc.entities.len()
}

/// Returns the human-readable name of an [`IgesSection`].
#[allow(dead_code)]
pub fn iges_section_name(section: IgesSection) -> &'static str {
    match section {
        IgesSection::Start => "Start",
        IgesSection::Global => "Global",
        IgesSection::Directory => "Directory",
        IgesSection::Parameter => "Parameter",
        IgesSection::Terminate => "Terminate",
    }
}

/// Returns only the IGES Start section as a string.
#[allow(dead_code)]
pub fn iges_header_string(doc: &IgesDocument) -> String {
    format!(
        "{}S      1\n",
        iges_section_line(
            &format!("{};", doc.config.description),
            'S',
            1
        )
    )
}

/// Removes all entities from the document and resets the sequence counter.
#[allow(dead_code)]
pub fn iges_document_clear(doc: &mut IgesDocument) {
    doc.entities.clear();
    doc.next_seq = 1;
}

// ── Private helpers ───────────────────────────────────────────────────────────

fn iges_section_line(content: &str, section: char, _seq: u32) -> String {
    // IGES lines are 80 chars wide; section letter at col 73, seq at 74-80.
    let max_content = 72;
    let trimmed: String = content.chars().take(max_content).collect();
    format!("{:<72}{}", trimmed, section)
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    #[test]
    fn test_default_config_fields() {
        let cfg = default_iges_config();
        assert!(!cfg.author.is_empty());
        assert!(!cfg.organisation.is_empty());
        assert!(!cfg.originating_system.is_empty());
        assert!(!cfg.version.is_empty());
    }

    #[test]
    fn test_new_document_empty() {
        let cfg = default_iges_config();
        let doc = new_iges_document(&cfg);
        assert_eq!(iges_entity_count(&doc), 0);
    }

    #[test]
    fn test_add_point_increments_count() {
        let cfg = default_iges_config();
        let mut doc = new_iges_document(&cfg);
        iges_add_point(&mut doc, 1.0, 2.0, 3.0);
        assert_eq!(iges_entity_count(&doc), 1);
    }

    #[test]
    fn test_add_point_returns_sequential_ids() {
        let cfg = default_iges_config();
        let mut doc = new_iges_document(&cfg);
        let id1 = iges_add_point(&mut doc, 0.0, 0.0, 0.0);
        let id2 = iges_add_point(&mut doc, 1.0, 0.0, 0.0);
        assert_eq!(id1, 1);
        assert_eq!(id2, 2);
    }

    #[test]
    fn test_add_line_entity_type() {
        let cfg = default_iges_config();
        let mut doc = new_iges_document(&cfg);
        iges_add_line(&mut doc, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
        assert_eq!(doc.entities[0].entity_type, 110);
    }

    #[test]
    fn test_iges_to_string_contains_terminate() {
        let cfg = default_iges_config();
        let doc = new_iges_document(&cfg);
        let s = iges_to_string(&doc);
        assert!(s.contains('T'));
    }

    #[test]
    fn test_iges_to_string_has_start_section() {
        let cfg = default_iges_config();
        let doc = new_iges_document(&cfg);
        let s = iges_to_string(&doc);
        assert!(s.contains('S'));
    }

    #[test]
    fn test_iges_section_name_all_variants() {
        assert_eq!(iges_section_name(IgesSection::Start), "Start");
        assert_eq!(iges_section_name(IgesSection::Global), "Global");
        assert_eq!(iges_section_name(IgesSection::Directory), "Directory");
        assert_eq!(iges_section_name(IgesSection::Parameter), "Parameter");
        assert_eq!(iges_section_name(IgesSection::Terminate), "Terminate");
    }

    #[test]
    fn test_iges_header_string_not_empty() {
        let cfg = default_iges_config();
        let doc = new_iges_document(&cfg);
        let hdr = iges_header_string(&doc);
        assert!(!hdr.is_empty());
    }

    #[test]
    fn test_document_clear_resets_count() {
        let cfg = default_iges_config();
        let mut doc = new_iges_document(&cfg);
        iges_add_point(&mut doc, 0.0, 0.0, 0.0);
        iges_document_clear(&mut doc);
        assert_eq!(iges_entity_count(&doc), 0);
    }

    #[test]
    fn test_clear_resets_sequence() {
        let cfg = default_iges_config();
        let mut doc = new_iges_document(&cfg);
        iges_add_point(&mut doc, 0.0, 0.0, 0.0);
        iges_document_clear(&mut doc);
        let id = iges_add_point(&mut doc, 1.0, 2.0, 3.0);
        assert_eq!(id, 1);
    }

    #[test]
    fn test_point_entity_type() {
        let cfg = default_iges_config();
        let mut doc = new_iges_document(&cfg);
        iges_add_point(&mut doc, 5.0, 6.0, 7.0);
        assert_eq!(doc.entities[0].entity_type, 116);
    }

    #[test]
    fn test_line_param_data_contains_coords() {
        let cfg = default_iges_config();
        let mut doc = new_iges_document(&cfg);
        iges_add_line(&mut doc, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
        assert!(doc.entities[0].param_data.contains("1.000000"));
        assert!(doc.entities[0].param_data.contains("6.000000"));
    }
}