oxihuman-export 0.1.2

Export pipeline for OxiHuman — glTF, COLLADA, STL, and streaming formats
Documentation
//! STEP (ISO 10303) CAD format export stub.
//!
//! Generates minimal STEP ASCII header and entity stubs for CAD interchange.

use std::fmt::Write as FmtWrite;

// ── Config ────────────────────────────────────────────────────────────────────

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

// ── Entity ────────────────────────────────────────────────────────────────────

/// A single STEP entity (e.g. `CARTESIAN_POINT`, `ADVANCED_FACE`, …).
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct StepEntity {
    /// Entity reference number (e.g. `#1`).
    pub id: u32,
    /// Entity type keyword (e.g. `"CARTESIAN_POINT"`).
    pub entity_type: String,
    /// Positional / keyword parameters as strings.
    pub params: Vec<String>,
}

// ── Document ──────────────────────────────────────────────────────────────────

/// In-memory STEP document.
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct StepDocument {
    /// Export configuration.
    pub config: StepExportConfig,
    /// Ordered list of entities.
    pub entities: Vec<StepEntity>,
    /// Counter for the next entity id.
    next_id: u32,
}

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

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

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

/// Appends a raw entity to the document and returns its assigned id.
#[allow(dead_code)]
pub fn step_add_entity(
    doc: &mut StepDocument,
    entity_type: &str,
    params: Vec<String>,
) -> u32 {
    let id = doc.next_id;
    doc.next_id += 1;
    doc.entities.push(StepEntity {
        id,
        entity_type: entity_type.to_string(),
        params,
    });
    id
}

/// Serialises the document to a STEP ASCII string.
#[allow(dead_code)]
pub fn step_to_string(doc: &StepDocument) -> String {
    let mut out = String::new();
    out.push_str(&step_header_string(doc));
    out.push_str("DATA;\n");
    for ent in &doc.entities {
        let params = ent.params.join(",");
        let _ = writeln!(out, "#{} = {}({});", ent.id, ent.entity_type, params);
    }
    out.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
    out
}

/// Writes the STEP document to a file at `path`.
#[allow(dead_code)]
pub fn step_write_to_file(doc: &StepDocument, path: &str) -> Result<(), String> {
    let content = step_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 step_entity_count(doc: &StepDocument) -> usize {
    doc.entities.len()
}

/// Convenience helper: adds a `CARTESIAN_POINT` entity.
#[allow(dead_code)]
pub fn step_add_cartesian_point(doc: &mut StepDocument, x: f64, y: f64, z: f64) -> u32 {
    step_add_entity(
        doc,
        "CARTESIAN_POINT",
        vec![
            "''".to_string(),
            format!("({x:.6},{y:.6},{z:.6})"),
        ],
    )
}

/// Convenience helper: adds an `ADVANCED_FACE` entity referencing point ids.
#[allow(dead_code)]
pub fn step_add_face(doc: &mut StepDocument, point_refs: &[u32]) -> u32 {
    let refs: Vec<String> = point_refs.iter().map(|r| format!("#{r}")).collect();
    let list = format!("({})", refs.join(","));
    step_add_entity(doc, "ADVANCED_FACE", vec!["''".to_string(), list, ".T.".to_string()])
}

/// Returns only the STEP ASCII header section as a string.
#[allow(dead_code)]
pub fn step_header_string(doc: &StepDocument) -> String {
    format!(
        "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION(('{}'),'2;1');\n\
         FILE_NAME('','',('{}'),('{}'),'{}','OxiHuman','');\n\
         FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));\nENDSEC;\n",
        doc.config.description,
        doc.config.author,
        doc.config.organisation,
        doc.config.originating_system,
    )
}

/// Removes all entities from the document and resets the id counter.
#[allow(dead_code)]
pub fn step_document_clear(doc: &mut StepDocument) {
    doc.entities.clear();
    doc.next_id = 1;
}

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

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

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

    #[test]
    fn test_new_document_empty() {
        let cfg = default_step_config();
        let doc = new_step_document(&cfg);
        assert_eq!(step_entity_count(&doc), 0);
    }

    #[test]
    fn test_add_entity_increments_count() {
        let cfg = default_step_config();
        let mut doc = new_step_document(&cfg);
        step_add_entity(&mut doc, "CARTESIAN_POINT", vec!["''".to_string()]);
        assert_eq!(step_entity_count(&doc), 1);
    }

    #[test]
    fn test_add_entity_ids_sequential() {
        let cfg = default_step_config();
        let mut doc = new_step_document(&cfg);
        let id1 = step_add_entity(&mut doc, "TYPE_A", vec![]);
        let id2 = step_add_entity(&mut doc, "TYPE_B", vec![]);
        assert_eq!(id1, 1);
        assert_eq!(id2, 2);
    }

    #[test]
    fn test_step_to_string_contains_header() {
        let cfg = default_step_config();
        let doc = new_step_document(&cfg);
        let s = step_to_string(&doc);
        assert!(s.contains("ISO-10303-21;"));
        assert!(s.contains("HEADER;"));
        assert!(s.contains("END-ISO-10303-21;"));
    }

    #[test]
    fn test_step_add_cartesian_point() {
        let cfg = default_step_config();
        let mut doc = new_step_document(&cfg);
        let id = step_add_cartesian_point(&mut doc, 1.0, 2.0, 3.0);
        assert_eq!(id, 1);
        let s = step_to_string(&doc);
        assert!(s.contains("CARTESIAN_POINT"));
    }

    #[test]
    fn test_step_add_face() {
        let cfg = default_step_config();
        let mut doc = new_step_document(&cfg);
        let p1 = step_add_cartesian_point(&mut doc, 0.0, 0.0, 0.0);
        let p2 = step_add_cartesian_point(&mut doc, 1.0, 0.0, 0.0);
        let p3 = step_add_cartesian_point(&mut doc, 0.0, 1.0, 0.0);
        let face_id = step_add_face(&mut doc, &[p1, p2, p3]);
        assert_eq!(face_id, 4);
        assert_eq!(step_entity_count(&doc), 4);
    }

    #[test]
    fn test_document_clear() {
        let cfg = default_step_config();
        let mut doc = new_step_document(&cfg);
        step_add_cartesian_point(&mut doc, 0.0, 0.0, 0.0);
        step_document_clear(&mut doc);
        assert_eq!(step_entity_count(&doc), 0);
        // After clear, ids restart from 1.
        let id = step_add_entity(&mut doc, "RESET_CHECK", vec![]);
        assert_eq!(id, 1);
    }

    #[test]
    fn test_step_header_string_contains_author() {
        let mut cfg = default_step_config();
        cfg.author = "TestAuthor".to_string();
        let doc = new_step_document(&cfg);
        let hdr = step_header_string(&doc);
        assert!(hdr.contains("TestAuthor"));
    }
}