wow-adt 0.6.4

Parser for World of Warcraft ADT terrain files with heightmap and texture layer support
Documentation
// model_export.rs - Export terrain to 3D model formats

use crate::Adt;
use crate::error::Result;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;

/// Options for 3D model export
#[derive(Debug, Clone)]
pub struct ModelExportOptions {
    /// Export format
    pub format: ModelFormat,
    /// Scale factor for coordinates
    pub scale: f32,
    /// Whether to invert Z axis
    pub invert_z: bool,
    /// Whether to include texture coordinates
    pub include_uvs: bool,
    /// Whether to include normal vectors
    pub include_normals: bool,
    /// Whether to include materials
    pub include_materials: bool,
    /// Whether to split the model into chunks
    pub split_chunks: bool,
    /// Whether to optimize the model by removing duplicate vertices
    pub optimize: bool,
}

/// Format for 3D model export
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelFormat {
    /// Wavefront OBJ format
    OBJ,
    /// Stanford PLY format
    PLY,
    /// STL format
    STL,
}

impl Default for ModelExportOptions {
    fn default() -> Self {
        Self {
            format: ModelFormat::OBJ,
            scale: 1.0,
            invert_z: false,
            include_uvs: true,
            include_normals: true,
            include_materials: true,
            split_chunks: false,
            optimize: true,
        }
    }
}

/// Export an ADT to a 3D model format
pub fn export_to_3d<P: AsRef<Path>>(
    adt: &Adt,
    output_path: P,
    options: ModelExportOptions,
) -> Result<()> {
    match options.format {
        ModelFormat::OBJ => export_to_obj(adt, output_path, &options),
        ModelFormat::PLY => export_to_ply(adt, output_path, &options),
        ModelFormat::STL => export_to_stl(adt, output_path, &options),
    }
}

/// Export an ADT to OBJ format
fn export_to_obj<P: AsRef<Path>>(
    adt: &Adt,
    output_path: P,
    options: &ModelExportOptions,
) -> Result<()> {
    let path = output_path.as_ref();
    let file = File::create(path)?;
    let mut writer = BufWriter::new(file);

    // Write OBJ header
    writeln!(writer, "# WoW ADT Terrain")?;
    writeln!(writer, "# Generated by wow_adt")?;
    writeln!(writer, "# Version: {}", adt.version())?;
    writeln!(writer)?;

    // If including materials, create MTL file
    if options.include_materials {
        let mtl_path = path.with_extension("mtl");
        let mtl_filename = mtl_path
            .file_name()
            .expect("path should have a file name component")
            .to_string_lossy();

        writeln!(writer, "mtllib {mtl_filename}")?;
        writeln!(writer)?;

        export_to_mtl(adt, &mtl_path, options)?;
    }

    // Vertex index offset (OBJ indices are 1-based)
    let mut vertex_offset = 1;
    let mut normal_offset = 1;
    let mut uv_offset = 1;

    // Process each MCNK
    for chunk_idx in 0..adt.mcnk_chunks.len() {
        let chunk = &adt.mcnk_chunks[chunk_idx];

        // If splitting by chunks, add object name
        if options.split_chunks {
            let chunk_y = chunk_idx / 16;
            let chunk_x = chunk_idx % 16;
            writeln!(writer, "o chunk_{chunk_x}_{chunk_y}")?;
        }

        // If including materials, add material reference
        if options.include_materials {
            // Use first texture layer as material
            if !chunk.texture_layers.is_empty() {
                let layer = &chunk.texture_layers[0];

                // Get material name from MTEX if available
                let material_name = if let Some(ref mtex) = adt.mtex {
                    if (layer.texture_id as usize) < mtex.filenames.len() {
                        // Extract basename without extension
                        let filename = &mtex.filenames[layer.texture_id as usize];
                        let basename = filename.split('/').next_back().unwrap_or(filename);
                        basename.split('.').next().unwrap_or(basename).to_string()
                    } else {
                        format!("material_{}", layer.texture_id)
                    }
                } else {
                    format!("material_{}", layer.texture_id)
                };

                writeln!(writer, "usemtl {material_name}")?;
            } else {
                // Default material
                writeln!(writer, "usemtl default")?;
            }
        }

        // Get chunk position and height data
        let _chunk_x = chunk.ix as f32 * 533.333_3; // MCNK grid is 533.33 units
        let _chunk_y = chunk.iy as f32 * 533.333_3;
        let chunk_pos = [chunk.position[0], chunk.position[1], chunk.position[2]];

        // Define vertices (heightmap is 9x9 grid)
        let mut vertices = Vec::new();
        let mut normals = Vec::new();
        let mut texture_coords = Vec::new();

        // Extract height map and normals
        for y in 0..9 {
            for x in 0..9 {
                let height_idx = y * 9 + x;

                if height_idx < chunk.height_map.len() {
                    // Get height value
                    let height = chunk.height_map[height_idx];

                    // Calculate position
                    let pos_x = chunk_pos[0] + (x as f32 * 533.333_3 / 8.0);
                    let pos_y = chunk_pos[1] + (y as f32 * 533.333_3 / 8.0);
                    let pos_z = if options.invert_z { -height } else { height };

                    // Scale coordinates
                    let scaled_x = pos_x * options.scale;
                    let scaled_y = pos_y * options.scale;
                    let scaled_z = pos_z * options.scale;

                    // Add vertex
                    vertices.push([scaled_x, scaled_y, scaled_z]);

                    // Add normal if available
                    if options.include_normals && height_idx < chunk.normals.len() {
                        let normal = chunk.normals[height_idx];

                        // Convert from [-127, 127] to [-1, 1]
                        let nx = normal[0] as f32 / 127.0;
                        let ny = normal[1] as f32 / 127.0;
                        let mut nz = normal[2] as f32 / 127.0;

                        // Invert Z if needed
                        if options.invert_z {
                            nz = -nz;
                        }

                        normals.push([nx, ny, nz]);
                    }

                    // Add texture coordinates
                    if options.include_uvs {
                        // Map coordinates to [0, 1] range
                        let u = x as f32 / 8.0;
                        let v = y as f32 / 8.0;

                        texture_coords.push([u, v]);
                    }
                }
            }
        }

        // Write vertices
        for vertex in &vertices {
            writeln!(writer, "v {} {} {}", vertex[0], vertex[1], vertex[2])?;
        }

        // Write texture coordinates
        if options.include_uvs {
            for uv in &texture_coords {
                writeln!(writer, "vt {} {}", uv[0], uv[1])?;
            }
        }

        // Write normals
        if options.include_normals {
            for normal in &normals {
                writeln!(writer, "vn {} {} {}", normal[0], normal[1], normal[2])?;
            }
        }

        // Write faces (8x8 grid of quads)
        for y in 0..8 {
            for x in 0..8 {
                // Get vertex indices for this quad
                let v1 = y * 9 + x;
                let v2 = y * 9 + (x + 1);
                let v3 = (y + 1) * 9 + (x + 1);
                let v4 = (y + 1) * 9 + x;

                // Add offset to indices
                let v1_idx = v1 + vertex_offset;
                let v2_idx = v2 + vertex_offset;
                let v3_idx = v3 + vertex_offset;
                let v4_idx = v4 + vertex_offset;

                // Write face with appropriate indices
                if options.include_uvs && options.include_normals {
                    let t1_idx = v1 + uv_offset;
                    let t2_idx = v2 + uv_offset;
                    let t3_idx = v3 + uv_offset;
                    let t4_idx = v4 + uv_offset;

                    let n1_idx = v1 + normal_offset;
                    let n2_idx = v2 + normal_offset;
                    let n3_idx = v3 + normal_offset;
                    let n4_idx = v4 + normal_offset;

                    // f v1/t1/n1 v2/t2/n2 v3/t3/n3
                    writeln!(
                        writer,
                        "f {v1_idx}/{t1_idx}/{n1_idx} {v2_idx}/{t2_idx}/{n2_idx} {v3_idx}/{t3_idx}/{n3_idx} {v4_idx}/{t4_idx}/{n4_idx}"
                    )?;
                } else if options.include_uvs {
                    let t1_idx = v1 + uv_offset;
                    let t2_idx = v2 + uv_offset;
                    let t3_idx = v3 + uv_offset;
                    let t4_idx = v4 + uv_offset;

                    // f v1/t1 v2/t2 v3/t3
                    writeln!(
                        writer,
                        "f {v1_idx}/{t1_idx} {v2_idx}/{t2_idx} {v3_idx}/{t3_idx} {v4_idx}/{t4_idx}"
                    )?;
                } else if options.include_normals {
                    let n1_idx = v1 + normal_offset;
                    let n2_idx = v2 + normal_offset;
                    let n3_idx = v3 + normal_offset;
                    let n4_idx = v4 + normal_offset;

                    // f v1//n1 v2//n2 v3//n3
                    writeln!(
                        writer,
                        "f {v1_idx}//{n1_idx}  {v2_idx}//{n2_idx}  {v3_idx}//{n3_idx}  {v4_idx}//{n4_idx}"
                    )?;
                } else {
                    // f v1 v2 v3
                    writeln!(writer, "f {v1_idx} {v2_idx} {v3_idx} {v4_idx}")?;
                }
            }
        }

        // Update offsets for next chunk
        vertex_offset += vertices.len();
        if options.include_normals {
            normal_offset += normals.len();
        }
        if options.include_uvs {
            uv_offset += texture_coords.len();
        }
    }

    Ok(())
}

/// Export MTL file for OBJ material definitions
fn export_to_mtl<P: AsRef<Path>>(
    adt: &Adt,
    mtl_path: P,
    _options: &ModelExportOptions,
) -> Result<()> {
    let file = File::create(mtl_path)?;
    let mut writer = BufWriter::new(file);

    // Write MTL header
    writeln!(writer, "# WoW ADT Material Library")?;
    writeln!(writer, "# Generated by wow_adt")?;
    writeln!(writer)?;

    // Default material
    writeln!(writer, "newmtl default")?;
    writeln!(writer, "Ka 0.2 0.2 0.2")?; // Ambient color
    writeln!(writer, "Kd 0.8 0.8 0.8")?; // Diffuse color
    writeln!(writer, "Ks 0.0 0.0 0.0")?; // Specular color
    writeln!(writer, "Ns 0.0")?; // Shininess
    writeln!(writer)?;

    // Process texture materials from MTEX
    if let Some(ref mtex) = adt.mtex {
        for filename in mtex.filenames.iter() {
            // Extract basename without extension
            let basename = filename.split('/').next_back().unwrap_or(filename);
            let material_name = basename.split('.').next().unwrap_or(basename);

            writeln!(writer, "newmtl {material_name}")?;
            writeln!(writer, "Ka 0.2 0.2 0.2")?; // Ambient color
            writeln!(writer, "Kd 0.8 0.8 0.8")?; // Diffuse color
            writeln!(writer, "Ks 0.0 0.0 0.0")?; // Specular color
            writeln!(writer, "Ns 0.0")?; // Shininess

            // Reference texture if needed
            writeln!(writer, "# Original texture: {filename}")?;
            // Note: We don't know the actual texture file path, so we don't include map_Kd

            writeln!(writer)?;
        }
    }

    Ok(())
}

/// Export an ADT to PLY format
fn export_to_ply<P: AsRef<Path>>(
    _adt: &Adt,
    _output_path: P,
    _options: &ModelExportOptions,
) -> Result<()> {
    // Implement PLY export
    // For now, return not implemented
    Err(crate::error::AdtError::NotImplemented(
        "PLY export is not yet implemented".to_string(),
    ))
}

/// Export an ADT to STL format
fn export_to_stl<P: AsRef<Path>>(
    _adt: &Adt,
    _output_path: P,
    _options: &ModelExportOptions,
) -> Result<()> {
    // Implement STL export
    // For now, return not implemented
    Err(crate::error::AdtError::NotImplemented(
        "STL export is not yet implemented".to_string(),
    ))
}