mmd-anim-cli 0.1.6

CLI diagnostics and roundtrip tools for mmd-anim
use std::{collections::BTreeMap, path::Path, process::ExitCode, sync::Arc};

use glam::Vec3A;
use mmd_anim_runtime::RuntimeInstance;

use crate::{diagnostics_suffix, f32_checksum, read_file, translation_checksum};

pub(crate) fn import_pmx_summary(path: &Path) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let data = read_file(path)?;
    let imported = mmd_anim_format::import_pmx_runtime(&data)?;
    println!(
        "PMX runtime import: bones={} append={} fixedAxis={} ik={} boneNames={} morphNames={} ikNameMap={}",
        imported.model.bone_count(),
        imported.model.append_transforms().len(),
        imported.model.fixed_axis_count(),
        imported.model.ik_count(),
        imported.bone_name_to_index.len(),
        imported.morph_name_to_index.len(),
        imported.ik_solver_bone_name_to_index.len()
    );
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn import_pmx_ik_summary(path: &Path) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let data = read_file(path)?;
    let imported = mmd_anim_format::import_pmx_runtime(&data)?;
    let solvers = imported.model.ik_solvers();
    let max_iterations = solvers
        .iter()
        .map(|solver| solver.iteration_count)
        .max()
        .unwrap_or(0);
    let mut distribution = BTreeMap::<u32, usize>::new();
    for solver in solvers {
        *distribution.entry(solver.iteration_count).or_default() += 1;
    }
    let distribution = distribution
        .iter()
        .map(|(iterations, count)| format!("{iterations}:{count}"))
        .collect::<Vec<_>>()
        .join(",");

    println!(
        "PMX IK summary: bones={} ik={} maxIterations={} distribution={}",
        imported.model.bone_count(),
        solvers.len(),
        max_iterations,
        distribution
    );
    for (solver_index, solver) in solvers.iter().enumerate() {
        if solver.iteration_count == max_iterations {
            let name = imported
                .bone_names
                .get(solver.ik_bone.as_usize())
                .map(String::as_str)
                .unwrap_or("<unknown>");
            println!(
                "max IK: solver={} bone={} name={} target={} iterations={} limitAngle={:.6} links={}",
                solver_index,
                solver.ik_bone.as_usize(),
                name,
                solver.target_bone.as_usize(),
                solver.iteration_count,
                solver.limit_angle,
                solver.links.len()
            );
        }
    }
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn import_pmd_summary(path: &Path) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let data = read_file(path)?;
    let imported = mmd_anim_format::import_pmd_runtime(&data)?;
    println!(
        "PMD runtime import: bones={} ik={} morphSlots={} vertexMorphOffsets={} boneNames={} morphNames={} ikNameMap={}{}",
        imported.model.bone_count(),
        imported.model.ik_count(),
        imported.model.morph_count(),
        imported.model.vertex_morph_offsets().len(),
        imported.bone_name_to_index.len(),
        imported.morph_name_to_index.len(),
        imported.ik_solver_bone_name_to_index.len(),
        diagnostics_suffix(imported.diagnostics.len())
    );
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn import_vmd_summary(path: &Path) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let data = read_file(path)?;
    let imported = mmd_anim_format::import_vmd_motion(&data)?;
    let property_ik_entries: usize = imported
        .property_ik_frames
        .iter()
        .map(|frame| frame.entries.len())
        .sum();
    println!(
        "VMD runtime import: boneKeys={} morphKeys={} propertyFrames={} propertyIkEntries={}",
        imported.bone_keyframes.len(),
        imported.morph_keyframes.len(),
        imported.property_ik_frames.len(),
        property_ik_entries
    );
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn import_pair_summary(
    pmx_path: &Path,
    vmd_path: &Path,
) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let pmx = mmd_anim_format::import_pmx_runtime(&read_file(pmx_path)?)?;
    let vmd = mmd_anim_format::import_vmd_motion(&read_file(vmd_path)?)?;

    let matched_bone_keys = vmd
        .bone_keyframes
        .iter()
        .filter(|keyframe| {
            pmx.bone_name_to_index
                .contains_key(&keyframe.bone_name_normalized)
        })
        .count();
    let matched_morph_keys = vmd
        .morph_keyframes
        .iter()
        .filter(|(name, _, _)| {
            let normalized = mmd_anim_format::normalize_vmd_name(name);
            pmx.morph_name_to_index.contains_key(&normalized)
        })
        .count();
    let property_ik_entries: usize = vmd
        .property_ik_frames
        .iter()
        .map(|frame| frame.entries.len())
        .sum();
    let matched_property_ik_entries = vmd
        .property_ik_frames
        .iter()
        .flat_map(|frame| frame.entries.iter())
        .filter(|entry| {
            pmx.ik_solver_bone_name_to_index
                .contains_key(&entry.name_normalized)
        })
        .count();

    let bone_match_pct = if vmd.bone_keyframes.is_empty() {
        100.0
    } else {
        matched_bone_keys as f64 / vmd.bone_keyframes.len() as f64 * 100.0
    };
    let morph_match_pct = if vmd.morph_keyframes.is_empty() {
        100.0
    } else {
        matched_morph_keys as f64 / vmd.morph_keyframes.len() as f64 * 100.0
    };
    println!("PMX/VMD runtime import:");
    println!(
        "  model:    bones={} append={} fixedAxis={} ik={}",
        pmx.model.bone_count(),
        pmx.model.append_transforms().len(),
        pmx.model.fixed_axis_count(),
        pmx.model.ik_count(),
    );
    println!(
        "  motion:   vmdBoneKeys={} matchedBoneKeys={} ({:.1}%) vmdMorphKeys={} matchedMorphKeys={} ({:.1}%)",
        vmd.bone_keyframes.len(),
        matched_bone_keys,
        bone_match_pct,
        vmd.morph_keyframes.len(),
        matched_morph_keys,
        morph_match_pct,
    );
    println!(
        "  property: propertyFrames={} propertyIkEntries={} matchedPropertyIkEntries={}",
        vmd.property_ik_frames.len(),
        property_ik_entries,
        matched_property_ik_entries,
    );
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn import_pair_clip_summary(
    pmx_path: &Path,
    vmd_path: &Path,
) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let pmx = mmd_anim_format::import_pmx_runtime(&read_file(pmx_path)?)?;
    let vmd = mmd_anim_format::import_vmd_motion(&read_file(vmd_path)?)?;

    let solver_count = pmx.model.ik_count();
    let clip = mmd_anim_format::build_pair_clip(
        &vmd,
        &pmx.bone_name_to_index,
        &pmx.morph_name_to_index,
        &pmx.ik_solver_bone_name_to_index,
        solver_count,
    );

    let frame_range = clip
        .frame_range()
        .map(|(first, last)| format!("{first}..{last}"))
        .unwrap_or_else(|| "none".to_owned());

    println!(
        "Pair clip built: bones={} append={} fixedAxis={} ik={} clipBoneTracks={} clipMorphTracks={} propertyTrack={} frameRange={}",
        pmx.model.bone_count(),
        pmx.model.append_transforms().len(),
        pmx.model.fixed_axis_count(),
        pmx.model.ik_count(),
        clip.bone_track_count(),
        clip.morph_track_count(),
        clip.has_property_track(),
        frame_range
    );
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn import_pair_frame_summary(
    pmx_path: &Path,
    vmd_path: &Path,
    frame: f32,
) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let pmx = mmd_anim_format::import_pmx_runtime(&read_file(pmx_path)?)?;
    let vmd = mmd_anim_format::import_vmd_motion(&read_file(vmd_path)?)?;

    let bone_count = pmx.model.bone_count();
    let solver_count = pmx.model.ik_count();
    let morph_count = pmx
        .morph_name_to_index
        .values()
        .map(|index| index.as_usize() + 1)
        .max()
        .unwrap_or(0);

    let clip = mmd_anim_format::build_pair_clip(
        &vmd,
        &pmx.bone_name_to_index,
        &pmx.morph_name_to_index,
        &pmx.ik_solver_bone_name_to_index,
        solver_count,
    );

    let model = Arc::new(pmx.model);
    let mut runtime = RuntimeInstance::new_with_counts(model, morph_count, solver_count);

    runtime.evaluate_clip_frame(&clip, frame);

    let world_matrices = runtime.world_matrices();

    let first_translation = if let Some(m) = world_matrices.first() {
        Vec3A::new(m.w_axis.x, m.w_axis.y, m.w_axis.z)
    } else {
        Vec3A::ZERO
    };

    let checksum = translation_checksum(world_matrices);
    let morph_weights = runtime.morph_weights();
    let nonzero_morphs = morph_weights
        .iter()
        .filter(|weight| weight.abs() > f32::EPSILON)
        .count();
    let morph_checksum = f32_checksum(morph_weights);

    println!(
        "PMX/VMD frame {:.3}: bones={} ik={} clipBoneTracks={} clipMorphTracks={} worldMatrices={} firstTranslation=({:.6},{:.6},{:.6}) translationChecksum={:08x} nonzeroMorphs={} morphChecksum={:08x}",
        frame,
        bone_count,
        solver_count,
        clip.bone_track_count(),
        clip.morph_track_count(),
        world_matrices.len(),
        first_translation.x,
        first_translation.y,
        first_translation.z,
        checksum,
        nonzero_morphs,
        morph_checksum,
    );

    Ok(ExitCode::SUCCESS)
}