nlbn 1.0.12

Fast and reliable EasyEDA/LCSC to KiCad converter with batch processing support
Documentation
use crate::error::{KicadError, Result};
use std::collections::HashMap;

pub struct ModelExporter;

impl ModelExporter {
    pub fn new() -> Self {
        Self
    }

    /// Convert OBJ format to VRML (.wrl) format
    /// This follows the Python implementation logic exactly
    pub fn obj_to_wrl(&self, obj_data: &[u8]) -> Result<String> {
        let obj_str = String::from_utf8_lossy(obj_data);

        // Parse materials and vertices
        let materials = self.parse_obj_materials(&obj_str);
        let vertices = self.parse_obj_vertices(&obj_str)?;

        // Generate VRML output
        let mut output = String::new();
        output.push_str("#VRML V2.0 utf8\n");
        output.push_str("# 3D model generated by nlbn (https://github.com/linkyourbin/nlbn)\n");

        // Split by "usemtl" to process each material section
        let parts: Vec<&str> = obj_str.split("usemtl").collect();

        // Skip first part (before any usemtl)
        for shape_data in parts.iter().skip(1) {
            let lines: Vec<&str> = shape_data.lines().collect();
            if lines.is_empty() {
                continue;
            }

            // First line is the material name
            let material_name = lines[0].trim();
            let material = materials.get(material_name).cloned().unwrap_or_else(|| Material {
                diffuse: (0.8, 0.8, 0.8),
                specular: (0.5, 0.5, 0.5),
            });

            // Process faces for this material
            let mut index_counter = 0;
            let mut link_dict: HashMap<i32, i32> = HashMap::new();
            let mut coord_index = Vec::new();
            let mut points = Vec::new();

            for line in lines.iter().skip(1) {
                let line = line.trim();
                if line.starts_with("f ") {
                    // Parse face indices
                    let parts: Vec<&str> = line.split_whitespace().collect();
                    let mut face_index = Vec::new();

                    for part in parts.iter().skip(1) {
                        // Handle format like "1//" or "1"
                        let index_str = part.replace("//", "");
                        if let Ok(index) = index_str.parse::<i32>() {
                            // Check if we've seen this vertex before
                            if !link_dict.contains_key(&index) {
                                link_dict.insert(index, index_counter);
                                face_index.push(index_counter);
                                // Add vertex (OBJ indices are 1-based)
                                if (index as usize) <= vertices.len() {
                                    points.push(vertices[(index - 1) as usize].clone());
                                }
                                index_counter += 1;
                            } else {
                                face_index.push(*link_dict.get(&index).unwrap());
                            }
                        }
                    }

                    // Add -1 terminator
                    face_index.push(-1);
                    coord_index.push(face_index);
                }
            }

            // Skip if no faces found
            if points.is_empty() || coord_index.is_empty() {
                continue;
            }

            // Duplicate last point (Python does this: points.insert(-1, points[-1]))
            if points.len() > 0 {
                let last = points[points.len() - 1].clone();
                points.insert(points.len() - 1, last);
            }

            // Generate Shape for this material
            output.push_str("\nShape {\n");
            output.push_str("  appearance Appearance {\n");
            output.push_str("    material Material {\n");
            output.push_str(&format!("      diffuseColor {} {} {}\n",
                material.diffuse.0, material.diffuse.1, material.diffuse.2));
            output.push_str(&format!("      specularColor {} {} {}\n",
                material.specular.0, material.specular.1, material.specular.2));
            output.push_str("      ambientIntensity 0.2\n");
            output.push_str("      transparency 0\n");
            output.push_str("      shininess 0.5\n");
            output.push_str("    }\n");
            output.push_str("  }\n");
            output.push_str("  geometry IndexedFaceSet {\n");
            output.push_str("    ccw TRUE\n");
            output.push_str("    solid FALSE\n");
            output.push_str("    coord DEF co Coordinate {\n");
            output.push_str("      point [\n");

            // Write vertices
            for (i, vertex) in points.iter().enumerate() {
                if i < points.len() - 1 {
                    output.push_str(&format!("        {}, ", vertex));
                } else {
                    output.push_str(&format!("        {}\n", vertex));
                }
            }

            output.push_str("      ]\n");
            output.push_str("    }\n");
            output.push_str("    coordIndex [\n");

            // Write face indices
            for (i, face) in coord_index.iter().enumerate() {
                output.push_str("      ");
                for (j, idx) in face.iter().enumerate() {
                    if j < face.len() - 1 {
                        output.push_str(&format!("{}, ", idx));
                    } else {
                        output.push_str(&format!("{}", idx));
                    }
                }
                if i < coord_index.len() - 1 {
                    output.push_str(",\n");
                } else {
                    output.push_str("\n");
                }
            }

            output.push_str("    ]\n");
            output.push_str("  }\n");
            output.push_str("}\n");
        }

        Ok(output)
    }

    /// Export STEP file (just write binary data as-is)
    pub fn export_step(&self, step_data: &[u8]) -> Result<Vec<u8>> {
        Ok(step_data.to_vec())
    }

    fn parse_obj_vertices(&self, obj: &str) -> Result<Vec<String>> {
        let mut vertices = Vec::new();

        for line in obj.lines() {
            let line = line.trim();
            if line.starts_with("v ") {
                let parts: Vec<&str> = line.split_whitespace().collect();
                if parts.len() >= 4 {
                    let x = parts[1].parse::<f64>()
                        .map_err(|_| KicadError::ModelExport("Invalid vertex X coordinate".to_string()))?;
                    let y = parts[2].parse::<f64>()
                        .map_err(|_| KicadError::ModelExport("Invalid vertex Y coordinate".to_string()))?;
                    let z = parts[3].parse::<f64>()
                        .map_err(|_| KicadError::ModelExport("Invalid vertex Z coordinate".to_string()))?;

                    // Convert from mm to inches by dividing by 2.54, round to 4 decimals
                    let vx = format!("{:.4}", x / 2.54);
                    let vy = format!("{:.4}", y / 2.54);
                    let vz = format!("{:.4}", z / 2.54);

                    vertices.push(format!("{} {} {}", vx, vy, vz));
                }
            }
        }

        Ok(vertices)
    }

    fn parse_obj_materials(&self, obj: &str) -> HashMap<String, Material> {
        let mut materials = HashMap::new();
        let mut current_material: Option<(String, Material)> = None;

        for line in obj.lines() {
            let line = line.trim();

            if line.starts_with("newmtl ") {
                // Save previous material if exists
                if let Some((name, mat)) = current_material.take() {
                    materials.insert(name, mat);
                }

                // Start new material
                let parts: Vec<&str> = line.split_whitespace().collect();
                if parts.len() >= 2 {
                    current_material = Some((
                        parts[1].to_string(),
                        Material {
                            diffuse: (0.8, 0.8, 0.8),
                            specular: (0.5, 0.5, 0.5),
                        }
                    ));
                }
            } else if let Some((_, ref mut mat)) = current_material {
                if line.starts_with("Kd ") {
                    // Diffuse color
                    let parts: Vec<&str> = line.split_whitespace().collect();
                    if parts.len() >= 4 {
                        if let (Ok(r), Ok(g), Ok(b)) = (
                            parts[1].parse::<f64>(),
                            parts[2].parse::<f64>(),
                            parts[3].parse::<f64>()
                        ) {
                            mat.diffuse = (r, g, b);
                        }
                    }
                } else if line.starts_with("Ks ") {
                    // Specular color
                    let parts: Vec<&str> = line.split_whitespace().collect();
                    if parts.len() >= 4 {
                        if let (Ok(r), Ok(g), Ok(b)) = (
                            parts[1].parse::<f64>(),
                            parts[2].parse::<f64>(),
                            parts[3].parse::<f64>()
                        ) {
                            mat.specular = (r, g, b);
                        }
                    }
                } else if line == "endmtl" {
                    // End of material definition
                    if let Some((name, mat)) = current_material.take() {
                        materials.insert(name, mat);
                    }
                }
            }
        }

        // Save last material if not ended with endmtl
        if let Some((name, mat)) = current_material {
            materials.insert(name, mat);
        }

        materials
    }
}

impl Default for ModelExporter {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Clone)]
struct Material {
    diffuse: (f64, f64, f64),
    specular: (f64, f64, f64),
}