mxmxmx2tiles 0.1.0

Convert Bentley ContextCapture 3mx photogrammetry data to Cesium 3D Tiles
Documentation
//! 3mxb container: "3MXBO" + u32 header length + JSON header + concatenated
//! resource buffers (each `size` bytes, in header order).

use serde_json::Value;
use std::collections::HashMap;

pub struct Geometry {
    pub id: String,
    pub texture_id: Option<String>,
    pub ctm: Vec<u8>,
}

pub struct Tmxb {
    pub header: Value,
    pub textures: HashMap<String, Vec<u8>>,
    pub geometries: Vec<Geometry>,
}

pub fn parse(data: &[u8]) -> Result<Tmxb, String> {
    if data.len() < 9 || &data[0..5] != b"3MXBO" {
        return Err("bad magic (not a 3mxb)".into());
    }
    let header_len = u32::from_le_bytes(data[5..9].try_into().unwrap()) as usize;
    let header_end = 9 + header_len;
    if data.len() < header_end {
        return Err("truncated header".into());
    }
    let header: Value =
        serde_json::from_slice(&data[9..header_end]).map_err(|e| format!("header json: {e}"))?;

    let mut textures = HashMap::new();
    let mut geometries = Vec::new();

    let mut off = header_end;
    if let Some(resources) = header["resources"].as_array() {
        for r in resources {
            let size = r["size"].as_u64().unwrap_or(0) as usize;
            let end = off.checked_add(size).ok_or("resource size overflow")?;
            if end > data.len() {
                return Err(format!(
                    "resource buffer past end of file (off={off}, size={size}, len={})",
                    data.len()
                ));
            }
            let buf = &data[off..end];
            off = end;
            let id = r["id"].as_str().unwrap_or("").to_string();
            match r["type"].as_str() {
                Some("textureBuffer") => {
                    textures.insert(id, buf.to_vec());
                }
                Some("geometryBuffer") => match r["format"].as_str() {
                    Some("ctm") => geometries.push(Geometry {
                        id,
                        texture_id: r["texture"].as_str().map(|s| s.to_string()),
                        ctm: buf.to_vec(),
                    }),
                    other => eprintln!("  skip geometry format {other:?} (id={id})"),
                },
                other => eprintln!("  skip resource type {other:?} (id={id})"),
            }
        }
    }

    Ok(Tmxb {
        header,
        textures,
        geometries,
    })
}