draco-core 2.0.0

Pure Rust core encoder and decoder for Draco geometry compression
Documentation
use draco_core::decoder_buffer::DecoderBuffer;
use draco_core::draco_types::DataType;
use draco_core::encoder_buffer::EncoderBuffer;
use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
use draco_core::geometry_indices::{FaceIndex, PointIndex};
use draco_core::mesh::Mesh;
use draco_core::mesh_decoder::MeshDecoder;
use draco_core::mesh_encoder::MeshEncoder;
use draco_core::EncoderOptions;
use std::fs::File;
use std::io::Write;
use std::path::Path;

mod common;
use common::{optional_cpp_codec, optional_cpp_tool, ENCODER};
use std::process::Command;
use tempfile::Builder;

fn read_ply_header(path: &Path) -> std::io::Result<String> {
    let bytes = std::fs::read(path)?;
    // PLY header is always ASCII and terminated by end_header\n.
    let marker = b"end_header\n";
    let end = bytes
        .windows(marker.len())
        .position(|w| w == marker)
        .map(|pos| pos + marker.len())
        .unwrap_or(bytes.len());
    Ok(String::from_utf8_lossy(&bytes[..end]).into_owned())
}

fn create_torus_mesh() -> Mesh {
    let mut mesh = Mesh::new();
    mesh.set_num_points(4);
    mesh.set_num_faces(2);

    let mut pos_attr = PointAttribute::new();
    pos_attr.init(
        GeometryAttributeType::Position,
        3,
        DataType::Float32,
        false,
        4,
    );

    let coords: Vec<f32> = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0];

    for i in 0..4 {
        let offset = i * 3 * 4;
        pos_attr
            .buffer_mut()
            .update(&coords[i * 3].to_le_bytes(), Some(offset));
        pos_attr
            .buffer_mut()
            .update(&coords[i * 3 + 1].to_le_bytes(), Some(offset + 4));
        pos_attr
            .buffer_mut()
            .update(&coords[i * 3 + 2].to_le_bytes(), Some(offset + 8));
    }
    mesh.add_attribute(pos_attr);

    // f 0 1 2
    // f 0 2 3
    mesh.set_face(FaceIndex(0), [PointIndex(0), PointIndex(1), PointIndex(2)]);
    mesh.set_face(FaceIndex(1), [PointIndex(0), PointIndex(2), PointIndex(3)]);

    mesh
}

#[test]
fn test_rust_encode_cpp_decode() {
    let Some((encoder_path, decoder_path)) = optional_cpp_codec() else {
        return;
    };

    let temp_dir = Builder::new()
        .prefix("draco_test")
        .tempdir()
        .expect("Failed to create temp dir");
    let temp_path = temp_dir.path();

    let mesh = create_torus_mesh();

    // Write OBJ for C++ encoder
    let obj_path = temp_path.join("torus.obj");
    draco_io::obj_writer::write_obj_mesh(&obj_path, &mesh).expect("Failed to write OBJ");

    // Run C++ encoder
    let cpp_drc_path = temp_path.join("cpp_encoded.drc");
    let status = Command::new(&encoder_path)
        .arg("-i")
        .arg(&obj_path)
        .arg("-o")
        .arg(&cpp_drc_path)
        .arg("-method")
        .arg("edgebreaker")
        .arg("-qp")
        .arg("10")
        .status()
        .expect("Failed to run draco_encoder");
    assert!(status.success(), "C++ encoder failed");

    let mut options = EncoderOptions::new();
    options.set_global_int("encoding_method", 1); // Edgebreaker
    options.set_attribute_int(0, "quantization_bits", 10);

    let mut encoder = MeshEncoder::new();
    encoder.set_mesh(mesh.clone());
    let mut encoder_buffer = EncoderBuffer::new();
    encoder
        .encode(&options, &mut encoder_buffer)
        .expect("Encode failed");

    let drc_path = temp_path.join("rust_encoded.drc");
    let mut file = File::create(&drc_path).expect("Failed to create drc file");
    file.write_all(encoder_buffer.data())
        .expect("Failed to write drc file");

    // Compare files
    // We don't compare binary data directly because different encoders might produce different valid streams.
    // Instead we verify the C++ decoder can decode our output.

    let output = Command::new(&decoder_path)
        .arg("-i")
        .arg(&drc_path)
        .output()
        .expect("Failed to run draco_decoder");

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    println!("Decoder stdout: {}", stdout);
    println!("Decoder stderr: {}", stderr);

    assert!(output.status.success(), "C++ decoder failed");

    // The output path is given rather than left to the decoder: without `-o`
    // it names the file after the input, and the run would leave it wherever
    // the process happened to start.
    let decoded_ply_path = temp_path.join("rust_encoded.decoded.ply");
    let output = Command::new(&decoder_path)
        .arg("-i")
        .arg(&drc_path)
        .arg("-o")
        .arg(&decoded_ply_path)
        .output()
        .expect("Failed to run draco_decoder again");

    assert!(output.status.success(), "C++ decoder failed 2nd run");

    if decoded_ply_path.exists() {
        let ply_content =
            read_ply_header(&decoded_ply_path).expect("Failed to read PLY file header");
        assert!(
            ply_content.contains("element vertex 4"),
            "Decoded mesh has incorrect number of points"
        );
        assert!(
            ply_content.contains("element face 2"),
            "Decoded mesh has incorrect number of faces"
        );
    } else {
        // Fallback or failure
        println!(
            "Decoder output files not found at {}",
            decoded_ply_path.display()
        );
        panic!("PLY file not found");
    }
}

#[test]
fn test_rust_encode_rust_decode() {
    let mesh = create_torus_mesh();

    let mut options = EncoderOptions::new();
    options.set_global_int("encoding_method", 0); // Sequential
    options.set_attribute_int(0, "quantization_bits", 10);

    let mut encoder = MeshEncoder::new();
    encoder.set_mesh(mesh.clone());
    let mut encoder_buffer = EncoderBuffer::new();
    encoder
        .encode(&options, &mut encoder_buffer)
        .expect("Encode failed");

    let data = encoder_buffer.data();
    let mut decoder_buffer = DecoderBuffer::new(data);

    let mut decoder = MeshDecoder::new();
    let mut decoded_mesh = Mesh::new();
    let status = decoder.decode(&mut decoder_buffer, &mut decoded_mesh);

    assert!(status.is_ok(), "Rust decoder failed: {:?}", status);
    assert_eq!(decoded_mesh.num_points(), 4);
    assert_eq!(decoded_mesh.num_faces(), 2);
}

#[test]
fn test_cpp_encode_rust_decode() {
    let Some(encoder_path) = optional_cpp_tool(ENCODER) else {
        return;
    };

    let temp_dir = Builder::new()
        .prefix("draco_test_cpp")
        .tempdir()
        .expect("Failed to create temp dir");
    let temp_path = temp_dir.path();

    let mesh = create_torus_mesh();
    let obj_path = temp_path.join("temp_input.obj");
    draco_io::obj_writer::write_obj_mesh(&obj_path, &mesh).expect("Failed to write obj");

    let drc_path = temp_path.join("temp_cpp_out.drc");

    let output = Command::new(&encoder_path)
        .arg("-i")
        .arg(&obj_path)
        .arg("-o")
        .arg(&drc_path)
        .arg("-method")
        .arg("edgebreaker") // or "1"
        .arg("-cl")
        .arg("0")
        .arg("-qp")
        .arg("10") // quantization bits
        .output()
        .expect("Failed to run draco_encoder");

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    println!("Encoder stdout: {}", stdout);
    println!("Encoder stderr: {}", stderr);

    assert!(output.status.success(), "C++ encoder failed");

    let metadata = std::fs::metadata(&drc_path).unwrap();
    println!("C++ encoded size: {}", metadata.len());

    // Decode with Rust
    let mut file = File::open(&drc_path).expect("Failed to open drc file");
    let mut buffer = Vec::new();
    std::io::Read::read_to_end(&mut file, &mut buffer).expect("Failed to read drc file");

    let mut decoder = MeshDecoder::new();
    let mut decoder_buffer = DecoderBuffer::new(&buffer);
    let mut decoded_mesh = Mesh::new();

    let status = decoder.decode(&mut decoder_buffer, &mut decoded_mesh);
    match status {
        Ok(_) => {
            assert_eq!(decoded_mesh.num_faces(), mesh.num_faces());
            assert_eq!(decoded_mesh.num_points(), mesh.num_points());
            println!("Rust decoder successfully decoded C++ stream");
        }
        Err(e) => {
            panic!("Rust decoder failed: {:?}", e);
        }
    }
}

#[test]
fn test_rust_encode_rust_decode_edgebreaker() {
    let mesh = create_torus_mesh();

    let mut options = EncoderOptions::new();
    options.set_global_int("encoding_method", 1); // Edgebreaker
    options.set_attribute_int(0, "quantization_bits", 10);

    let mut encoder = MeshEncoder::new();
    encoder.set_mesh(mesh.clone());
    let mut encoder_buffer = EncoderBuffer::new();
    encoder
        .encode(&options, &mut encoder_buffer)
        .expect("Encode failed");

    let data = encoder_buffer.data();
    let mut decoder_buffer = DecoderBuffer::new(data);

    let mut decoder = MeshDecoder::new();
    let mut decoded_mesh = Mesh::new();
    let status = decoder.decode(&mut decoder_buffer, &mut decoded_mesh);

    assert!(status.is_ok(), "Rust decoder failed: {:?}", status);
    assert_eq!(decoded_mesh.num_points(), 4);
    assert_eq!(decoded_mesh.num_faces(), 2);
}