mmd-anim-cli 0.1.9

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

use clap::ValueEnum;
use serde::Serialize;

use crate::read_file;

#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub(crate) enum VmdSampleKind {
    Camera,
    Light,
    #[value(name = "self-shadow")]
    SelfShadow,
}

impl VmdSampleKind {
    fn label(self) -> &'static str {
        match self {
            Self::Camera => "camera",
            Self::Light => "light",
            Self::SelfShadow => "self-shadow",
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum VmdSampleState {
    Camera(mmd_anim_format::VmdCameraState),
    Light(mmd_anim_format::VmdLightState),
    SelfShadow(mmd_anim_format::VmdSelfShadowState),
}

impl VmdSampleState {
    fn kind(&self) -> VmdSampleKind {
        match self {
            Self::Camera(_) => VmdSampleKind::Camera,
            Self::Light(_) => VmdSampleKind::Light,
            Self::SelfShadow(_) => VmdSampleKind::SelfShadow,
        }
    }
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct VmdSampleJsonReport<'a> {
    kind: &'static str,
    motion: String,
    frame: f32,
    #[serde(skip_serializing_if = "Option::is_none")]
    camera: Option<&'a mmd_anim_format::VmdCameraState>,
    #[serde(skip_serializing_if = "Option::is_none")]
    light: Option<&'a mmd_anim_format::VmdLightState>,
    #[serde(skip_serializing_if = "Option::is_none")]
    self_shadow: Option<&'a mmd_anim_format::VmdSelfShadowState>,
}

pub(crate) fn vmd_sample(
    motion: &Path,
    kind: VmdSampleKind,
    frame: f32,
    json: bool,
) -> Result<ExitCode, Box<dyn std::error::Error>> {
    let data = read_file(motion)?;
    let state = sample_vmd_bytes(&data, kind, frame)?;
    if json {
        print_json(motion, frame, &state)?;
    } else {
        print_text(frame, state);
    }
    Ok(ExitCode::SUCCESS)
}

pub(crate) fn sample_vmd_bytes(
    data: &[u8],
    kind: VmdSampleKind,
    frame: f32,
) -> Result<VmdSampleState, Box<dyn std::error::Error>> {
    let parsed = mmd_anim_format::parse_vmd_animation(data)?;
    match kind {
        VmdSampleKind::Camera => {
            mmd_anim_format::sample_vmd_camera_frames(&parsed.camera_frames, frame)
                .map(VmdSampleState::Camera)
                .ok_or_else(|| "VMD has no camera keyframes".into())
        }
        VmdSampleKind::Light => {
            mmd_anim_format::sample_vmd_light_frames(&parsed.light_frames, frame)
                .map(VmdSampleState::Light)
                .ok_or_else(|| "VMD has no light keyframes".into())
        }
        VmdSampleKind::SelfShadow => {
            mmd_anim_format::sample_vmd_self_shadow_frames(&parsed.self_shadow_frames, frame)
                .map(VmdSampleState::SelfShadow)
                .ok_or_else(|| "VMD has no self-shadow keyframes".into())
        }
    }
}

fn print_json(
    motion: &Path,
    frame: f32,
    state: &VmdSampleState,
) -> Result<(), Box<dyn std::error::Error>> {
    let (camera, light, self_shadow) = match state {
        VmdSampleState::Camera(camera) => (Some(camera), None, None),
        VmdSampleState::Light(light) => (None, Some(light), None),
        VmdSampleState::SelfShadow(self_shadow) => (None, None, Some(self_shadow)),
    };
    let report = VmdSampleJsonReport {
        kind: state.kind().label(),
        motion: motion.display().to_string(),
        frame,
        camera,
        light,
        self_shadow,
    };
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

fn print_text(frame: f32, state: VmdSampleState) {
    match state {
        VmdSampleState::Camera(camera) => {
            println!(
                "VMD camera sample: frame={:.3} distance={:.6} position=({:.6},{:.6},{:.6}) rotation=({:.6},{:.6},{:.6}) fov={:.6} perspective={}",
                frame,
                camera.distance,
                camera.position[0],
                camera.position[1],
                camera.position[2],
                camera.rotation[0],
                camera.rotation[1],
                camera.rotation[2],
                camera.fov,
                camera.perspective
            );
        }
        VmdSampleState::Light(light) => {
            println!(
                "VMD light sample: frame={:.3} color=({:.6},{:.6},{:.6}) direction=({:.6},{:.6},{:.6})",
                frame,
                light.color[0],
                light.color[1],
                light.color[2],
                light.direction[0],
                light.direction[1],
                light.direction[2]
            );
        }
        VmdSampleState::SelfShadow(self_shadow) => {
            println!(
                "VMD self-shadow sample: frame={:.3} mode={} distance={:.6}",
                frame, self_shadow.mode, self_shadow.distance
            );
        }
    }
}