imodfile 0.1.0

A pure-Rust IMOD model file decoder/encoder — binary & ASCII, with Python bindings
Documentation
//! Integration tests for imodfile library.

use std::io::Cursor;
use imodfile::*;

/// Helper: create a simple test model with one object, two contours, and one mesh.
fn create_test_model() -> Imod {
    let mut model = Imod::default();
    model.name = "TestModel".to_string();
    model.xmax = 100;
    model.ymax = 200;
    model.zmax = 50;
    model.objsize = 1;

    // Create an object
    let mut obj = Iobj::default();
    obj.name = "Object1".to_string();
    obj.contsize = 2;
    obj.red = 1.0;
    obj.green = 0.5;
    obj.blue = 0.0;
    obj.flags = IMOD_OBJFLAG_DRAW_LABEL | IMOD_OBJFLAG_SCALE_WDTH | IMOD_OBJFLAG_FILL;

    // Contour 1: triangle
    let mut cont1 = Icont::default();
    cont1.psize = 3;
    cont1.pts = vec![
        Ipoint { x: 0.0, y: 0.0, z: 0.0 },
        Ipoint { x: 10.0, y: 0.0, z: 0.0 },
        Ipoint { x: 0.0, y: 10.0, z: 0.0 },
    ];
    cont1.surf = 0;

    // Contour 2: square
    let mut cont2 = Icont::default();
    cont2.psize = 4;
    cont2.pts = vec![
        Ipoint { x: 0.0, y: 0.0, z: 10.0 },
        Ipoint { x: 20.0, y: 0.0, z: 10.0 },
        Ipoint { x: 20.0, y: 20.0, z: 10.0 },
        Ipoint { x: 0.0, y: 20.0, z: 10.0 },
    ];
    cont2.surf = 0;
    cont2.flags = 1;
    cont2.time = 5;

    obj.cont = vec![cont1, cont2];

    // Mesh: simple quad
    let mut mesh = Imesh::default();
    mesh.vsize = 4;
    mesh.lsize = 5;
    mesh.vert = vec![
        Ipoint { x: 0.0, y: 0.0, z: 0.0 },
        Ipoint { x: 10.0, y: 0.0, z: 0.0 },
        Ipoint { x: 10.0, y: 10.0, z: 0.0 },
        Ipoint { x: 0.0, y: 10.0, z: 0.0 },
    ];
    mesh.list = vec![0, 1, 2, 3, -1]; // IMOD_MESH_ENDPOLY = -1
    mesh.flag = 0;
    obj.mesh = vec![mesh];
    obj.meshsize = 1;

    // Add a view
    let mut view = Iview::default();
    view.label = "Default View".to_string();
    view.fovy = 30.0;

    model.obj = vec![obj];
    model.view = vec![view];
    model.viewsize = 1;

    model
}

#[test]
fn test_binary_roundtrip() {
    let model = create_test_model();

    // Write to binary
    let mut buf = Vec::new();
    let mut cursor = Cursor::new(&mut buf);
    write_binary(&mut cursor, &model).expect("write_binary failed");

    // Read back
    cursor.set_position(0);
    let model2 = read_binary(&mut cursor).expect("read_binary failed");

    // Verify
    assert_eq!(model.name, model2.name);
    assert_eq!(model.obj.len(), model2.obj.len());
    assert_eq!(model.xmax, model2.xmax);
    assert_eq!(model.ymax, model2.ymax);
    assert_eq!(model.zmax, model2.zmax);

    let obj = &model.obj[0];
    let obj2 = &model2.obj[0];
    assert_eq!(obj.name, obj2.name);
    assert_eq!(obj.contsize, obj2.contsize);
    assert_eq!(obj.red, obj2.red);
    assert_eq!(obj.green, obj2.green);
    assert_eq!(obj.blue, obj2.blue);
    assert_eq!(obj.flags, obj2.flags);
    assert_eq!(obj.meshsize, obj2.meshsize);

    // Contours
    for i in 0..obj.contsize as usize {
        assert_eq!(obj.cont[i].psize, obj2.cont[i].psize);
        assert_eq!(obj.cont[i].surf, obj2.cont[i].surf);
        assert_eq!(obj.cont[i].flags, obj2.cont[i].flags);
        assert_eq!(obj.cont[i].time, obj2.cont[i].time);
        for j in 0..obj.cont[i].psize as usize {
            let a = obj.cont[i].pts[j];
            let b = obj2.cont[i].pts[j];
            assert!((a.x - b.x).abs() < 1e-5, "point {} cont {} x: {} != {}", j, i, a.x, b.x);
            assert!((a.y - b.y).abs() < 1e-5);
            assert!((a.z - b.z).abs() < 1e-5);
        }
    }

    // Meshes
    let mesh = &obj.mesh[0];
    let mesh2 = &obj2.mesh[0];
    assert_eq!(mesh.vsize, mesh2.vsize);
    assert_eq!(mesh.lsize, mesh2.lsize);
    assert_eq!(mesh.list, mesh2.list);
    for j in 0..mesh.vsize as usize {
        assert!(
            (mesh.vert[j].x - mesh2.vert[j].x).abs() < 1e-5,
            "mesh vert {} x: {} != {}",
            j,
            mesh.vert[j].x,
            mesh2.vert[j].x
        );
    }
}

#[test]
fn test_ascii_roundtrip() {
    let model = create_test_model();

    // Write to ASCII
    let mut buf = Vec::new();
    write_ascii(&mut buf, &model).expect("write_ascii failed");

    let ascii_text = String::from_utf8_lossy(&buf);
    println!("ASCII output:\n{}", ascii_text);

    // Read back from ASCII
    let model2 = read_ascii(&mut Cursor::new(&buf)).expect("read_ascii failed");

    // Verify basic properties
    assert_eq!(model.obj.len(), model2.obj.len());
    assert_eq!(model.xmax, model2.xmax);
    assert_eq!(model.ymax, model2.ymax);

    if !model2.obj.is_empty() {
        let obj2 = &model2.obj[0];
        assert_eq!(obj2.name, "Object1");
        assert!((obj2.red - 1.0).abs() < 0.01);
        assert!((obj2.green - 0.5).abs() < 0.01);
        assert_eq!(obj2.contsize, 2);
        assert_eq!(obj2.cont.len(), 2);

        // Check first contour
        if obj2.cont.len() > 0 {
            assert_eq!(obj2.cont[0].psize, 3);
            assert!(obj2.cont[0].pts.len() >= 3);
        }
    }
}

#[test]
fn test_binary_to_ascii() {
    let model = create_test_model();

    // Write binary
    let mut bin_buf = Vec::new();
    write_binary(&mut Cursor::new(&mut bin_buf), &model).expect("write_binary");

    // Read binary
    let model2 = read_binary(&mut Cursor::new(&bin_buf)).expect("read_binary");

    // Write ASCII from re-read model
    let mut ascii_buf = Vec::new();
    write_ascii(&mut ascii_buf, &model2).expect("write_ascii");

    let ascii_text = String::from_utf8_lossy(&ascii_buf);
    assert!(ascii_text.contains("imod 1"));
    assert!(ascii_text.contains("Object1"));
    assert!(ascii_text.contains("contour 0 0 3"));
    assert!(ascii_text.contains("contour 1 0 4"));
}

#[test]
fn test_create_empty_model() {
    let model = Imod::default();

    let mut buf = Vec::new();
    write_binary(&mut Cursor::new(&mut buf), &model).expect("write_binary empty");

    let model2 = read_binary(&mut Cursor::new(&buf)).expect("read_binary empty");
    assert_eq!(model2.objsize, 0);
    assert!(model2.obj.is_empty());
}

#[test]
fn test_model_with_labels() {
    let mut model = Imod::default();
    model.objsize = 1;

    let mut obj = Iobj::default();
    obj.name = "LabelTest".to_string();
    obj.contsize = 1;

    let mut cont = Icont::default();
    cont.psize = 2;
    cont.pts = vec![
        Ipoint { x: 0.0, y: 0.0, z: 0.0 },
        Ipoint { x: 1.0, y: 1.0, z: 1.0 },
    ];

    // Add label to contour
    let mut label = Ilabel::default();
    label.name = Some("ContourLabel".to_string());
    label.items.push(IlabelItem {
        name: "PointA".to_string(),
        index: 0,
    });
    label.items.push(IlabelItem {
        name: "PointB".to_string(),
        index: 1,
    });
    cont.label = Some(label);

    obj.cont = vec![cont];
    model.obj = vec![obj];

    // Binary round-trip
    let mut buf = Vec::new();
    write_binary(&mut Cursor::new(&mut buf), &model).expect("write_binary with labels");
    let model2 = read_binary(&mut Cursor::new(&buf)).expect("read_binary with labels");

    let cont2 = &model2.obj[0].cont[0];
    assert!(cont2.label.is_some());
    let label2 = cont2.label.as_ref().unwrap();
    assert_eq!(label2.name.as_deref(), Some("ContourLabel"));
    assert_eq!(label2.items.len(), 2);
    assert_eq!(label2.items[0].name, "PointA");
    assert_eq!(label2.items[1].name, "PointB");
}

#[test]
fn test_model_with_point_sizes() {
    let mut model = Imod::default();
    model.objsize = 1;

    let mut obj = Iobj::default();
    obj.name = "SizeTest".to_string();
    obj.contsize = 1;

    let mut cont = Icont::default();
    cont.psize = 3;
    cont.pts = vec![
        Ipoint { x: 0.0, y: 0.0, z: 0.0 },
        Ipoint { x: 1.0, y: 0.0, z: 0.0 },
        Ipoint { x: 0.0, y: 1.0, z: 0.0 },
    ];
    cont.sizes = Some(vec![1.5, 2.0, 0.5]);

    obj.cont = vec![cont];
    model.obj = vec![obj];

    let mut buf = Vec::new();
    write_binary(&mut Cursor::new(&mut buf), &model).expect("write_binary with sizes");
    let model2 = read_binary(&mut Cursor::new(&buf)).expect("read_binary with sizes");

    let cont2 = &model2.obj[0].cont[0];
    assert!(cont2.sizes.is_some());
    let sizes = cont2.sizes.as_ref().unwrap();
    assert_eq!(sizes.len(), 3);
    assert!((sizes[0] - 1.5).abs() < 1e-5);
    assert!((sizes[1] - 2.0).abs() < 1e-5);
    assert!((sizes[2] - 0.5).abs() < 1e-5);
}

#[test]
fn test_clip_planes() {
    let mut model = Imod::default();
    model.objsize = 1;

    let mut obj = Iobj::default();
    obj.name = "ClipTest".to_string();
    obj.contsize = 1;

    // Add a clip plane
    obj.clips.count = 1;
    obj.clips.flags = 1; // enabled
    obj.clips.normal[0] = Ipoint { x: 0.0, y: 0.0, z: 1.0 };
    obj.clips.point[0] = Ipoint { x: 0.0, y: 0.0, z: 50.0 };

    let mut cont = Icont::default();
    cont.psize = 1;
    cont.pts = vec![Ipoint { x: 10.0, y: 10.0, z: 10.0 }];
    obj.cont = vec![cont];
    model.obj = vec![obj];

    let mut buf = Vec::new();
    write_binary(&mut Cursor::new(&mut buf), &model).expect("write_binary with clips");
    let model2 = read_binary(&mut Cursor::new(&buf)).expect("read_binary with clips");

    let obj2 = &model2.obj[0];
    assert_eq!(obj2.clips.count, 1);
    assert!((obj2.clips.normal[0].x - 0.0).abs() < 1e-5);
    assert!((obj2.clips.normal[0].z - 1.0).abs() < 1e-5);
    assert!((obj2.clips.point[0].z - 50.0).abs() < 1e-5);
}

#[test]
fn test_file_read_write() {
    // Create a temp file path
    let tmp = std::env::temp_dir().join("test_imod_roundtrip.mod");

    let model = create_test_model();
    {
        let file = std::fs::File::create(&tmp).expect("create temp file");
        let mut writer = std::io::BufWriter::new(file);
        write_binary(&mut writer, &model).expect("write to file");
    }

    // Read back
    let file = std::fs::File::open(&tmp).expect("open temp file");
    let mut reader = std::io::BufReader::new(file);
    let model2 = read_binary(&mut reader).expect("read from file");
    assert_eq!(model2.obj.len(), 1);

    // Clean up
    let _ = std::fs::remove_file(&tmp);
}