mocari 0.1.0

A pure Rust Live2D/Cubism-compatible runtime, with an optional wgpu renderer
Documentation
use std::env;
use std::fs::File;
use std::io::Read;

use mocari::{
    json::{Cdi3, Model3, Motion3, Physics3},
    moc3::{
        Moc3ArtMeshKeyforms, Moc3ArtMeshes, Moc3CanvasInfo, Moc3CountInfo, Moc3Header, Moc3Ids,
        Moc3SectionOffsets, build_moc3_drawable_mesh, build_moc3_drawable_meshes,
    },
};
use zip::ZipArchive;

#[test]
fn parses_model3_from_opt_in_sample_zip() {
    let Some(mut archive) = open_sample_archive() else {
        return;
    };

    let model_json = read_first_text_entry(&mut archive, ".model3.json")
        .expect("sample zip contains a model3 json");
    let model = Model3::from_json_str(&model_json).expect("parse model3 json from sample zip");

    assert_eq!(model.version(), 3);
    assert!(!model.moc().is_empty());
    assert!(!model.textures().is_empty());
}

#[test]
fn parses_runtime_assets_from_opt_in_sample_zip() {
    let Some(mut archive) = open_sample_archive() else {
        return;
    };

    let cdi =
        read_first_text_entry(&mut archive, ".cdi3.json").expect("sample zip contains a cdi3 json");
    assert!(!Cdi3::from_json_str(&cdi).unwrap().parameters().is_empty());

    let physics = read_first_text_entry(&mut archive, ".physics3.json")
        .expect("sample zip contains a physics3 json");
    assert!(
        !Physics3::from_json_str(&physics)
            .unwrap()
            .settings()
            .is_empty()
    );

    let motions = read_all_text_entries(&mut archive, ".motion3.json");
    assert!(!motions.is_empty());
    for motion in motions {
        assert!(!Motion3::from_json_str(&motion).unwrap().curves().is_empty());
    }

    let moc3 = read_first_binary_entry(&mut archive, ".moc3").expect("sample zip contains a moc3");
    Moc3Header::parse(&moc3).unwrap();
    Moc3SectionOffsets::parse(&moc3).unwrap();
    let counts = Moc3CountInfo::parse(&moc3).unwrap();
    assert!(counts.parameters() > 0);
    assert!(counts.art_meshes() > 0);

    let ids = Moc3Ids::parse(&moc3).unwrap();
    assert_eq!(ids.parameters().len(), counts.parameters() as usize);
    assert_eq!(ids.art_meshes().len(), counts.art_meshes() as usize);

    let art_meshes = Moc3ArtMeshes::parse(&moc3).unwrap();
    assert_eq!(art_meshes.meshes().len(), counts.art_meshes() as usize);
    assert!(!art_meshes.uv_xys().is_empty());
    assert!(!art_meshes.position_indices().is_empty());

    let art_mesh_keyforms = Moc3ArtMeshKeyforms::parse(&moc3).unwrap();
    assert_eq!(
        art_mesh_keyforms.keyforms().len(),
        counts.art_mesh_keyforms() as usize
    );
    assert!(!art_mesh_keyforms.position_xys().is_empty());
    assert!(art_mesh_keyforms.art_mesh_keyform_positions(0, 0).is_some());
    assert!(build_moc3_drawable_mesh(&art_meshes, &art_mesh_keyforms, 0, 0).is_some());
    assert_eq!(
        build_moc3_drawable_meshes(&art_meshes, &art_mesh_keyforms)
            .unwrap()
            .len(),
        counts.art_meshes() as usize
    );

    let canvas = Moc3CanvasInfo::parse(&moc3).unwrap();
    assert!(canvas.width() > 0.0);
    assert!(canvas.height() > 0.0);
    assert!(canvas.pixels_per_unit() > 0.0);
}

fn open_sample_archive() -> Option<ZipArchive<File>> {
    let Some(zip_path) = env::var_os("MODEL_FIXTURE_ARCHIVE") else {
        eprintln!("skipping sample zip test; MODEL_FIXTURE_ARCHIVE is not set");
        return None;
    };

    let file = File::open(&zip_path).expect("open sample zip");
    Some(ZipArchive::new(file).expect("read sample zip"))
}

fn read_first_text_entry(archive: &mut ZipArchive<File>, suffix: &str) -> Option<String> {
    read_all_text_entries(archive, suffix).into_iter().next()
}

fn read_all_text_entries(archive: &mut ZipArchive<File>, suffix: &str) -> Vec<String> {
    let mut entries = Vec::new();

    for index in 0..archive.len() {
        let mut entry = archive.by_index(index).expect("read zip entry");
        if !entry.name().ends_with(suffix) {
            continue;
        }

        let mut source = String::new();
        entry
            .read_to_string(&mut source)
            .expect("read text entry from sample zip");
        entries.push(source);
    }

    entries
}

fn read_first_binary_entry(archive: &mut ZipArchive<File>, suffix: &str) -> Option<Vec<u8>> {
    for index in 0..archive.len() {
        let mut entry = archive.by_index(index).expect("read zip entry");
        if !entry.name().ends_with(suffix) {
            continue;
        }

        let mut bytes = Vec::new();
        entry
            .read_to_end(&mut bytes)
            .expect("read binary entry from sample zip");
        return Some(bytes);
    }

    None
}