mmd-mpl 0.2.10

MPL is a rule-based Domain-Specific Language for creating MMD poses and animations using natural semantic syntax
Documentation
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use crate::{animation::MPLAnimation, mpl::MPLKeyFrame, pose::MPLPose};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MPLScript {
    pub poses: HashMap<String, MPLPose>,
    pub animations: HashMap<String, MPLAnimation>,
    pub main: Vec<String>,
}

impl MPLScript {
    pub fn new() -> Self {
        Self {
            poses: HashMap::new(),
            animations: HashMap::new(),
            main: vec![],
        }
    }
    pub fn to_key_frames(&self) -> Vec<MPLKeyFrame> {
        let mut key_frames = vec![];
        for name in &self.main {
            if self.animations.contains_key(name) {
                let anim = self.animations.get(name).unwrap();
                for statement in &anim.statements {
                    let mut bone_frames = vec![];
                    if statement.poses.len() == 1 {
                        let pose_name = statement.poses[0].clone();
                        let pose = self.poses.get(&pose_name).unwrap();
                        bone_frames.extend(pose.to_bone_frames());
                    } else {
                        let mut pose_statements = vec![];
                        for pose_name in &statement.poses {
                            let pose = self.poses.get(pose_name).unwrap();
                            pose_statements.extend(pose.statements.clone());
                        }
                        let pose = MPLPose::new("composite".to_string(), pose_statements);
                        bone_frames.extend(pose.to_bone_frames());
                    }
                    key_frames.push(MPLKeyFrame::new(statement.time, bone_frames, vec![]));
                }
            } else if self.poses.contains_key(name) {
                let pose = self.poses.get(name).unwrap();
                key_frames.push(MPLKeyFrame::new(0.0, pose.to_bone_frames(), vec![]));
            }
        }
        key_frames
    }
}

enum BlockType {
    None,
    Pose,
    Animation,
    Main,
}

pub struct MPLCompiler {}

impl MPLCompiler {
    pub fn new() -> Self {
        Self {}
    }

    pub fn compile(&self, text: &str) -> Result<Vec<MPLKeyFrame>, String> {
        let mut in_block = false;
        let mut brace_count = 0;
        let mut current_block = String::new();
        let mut script = MPLScript::new();
        let mut block_type = BlockType::None;

        for (line_number, line) in text.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }
            if trimmed.starts_with("@pose")
                || trimmed.starts_with("@animation")
                || trimmed.starts_with("main")
            {
                if in_block {
                    return Err(format!(
                        "Line {}: Nested block is not allowed",
                        line_number + 1
                    ));
                }
                in_block = true;
                match trimmed.split_whitespace().next().unwrap() {
                    "@pose" => block_type = BlockType::Pose,
                    "@animation" => block_type = BlockType::Animation,
                    "main" => block_type = BlockType::Main,
                    _ => return Err(format!("Line {}: Invalid block type", line_number + 1)),
                }
            }

            if !in_block {
                return Err(format!(
                    "Line {}: Invalid text outside of block",
                    line_number + 1
                ));
            }
            current_block.push_str(line);
            current_block.push('\n');

            brace_count += line.chars().filter(|&c| c == '{').count() as i32;
            brace_count -= line.chars().filter(|&c| c == '}').count() as i32;

            if brace_count < 0 {
                return Err(format!(
                    "Line {}: Unexpected closing brace",
                    line_number + 1
                ));
            }
            if brace_count == 0 {
                match block_type {
                    BlockType::Pose => {
                        let pose = self.parse_pose(&current_block)?;

                        // Check for duplicate pose name
                        if script.poses.contains_key(&pose.name) {
                            return Err(format!("Duplicate pose name: '{}'", pose.name));
                        }
                        if script.animations.contains_key(&pose.name)
                            || script.poses.contains_key(&pose.name)
                        {
                            return Err(format!(
                                "Name '{}' already used by an animation",
                                pose.name
                            ));
                        }

                        script.poses.insert(pose.name.clone(), pose);
                    }
                    BlockType::Animation => {
                        let animation = self.parse_animation(&current_block)?;

                        // Check for duplicate animation name
                        if script.animations.contains_key(&animation.name)
                            || script.poses.contains_key(&animation.name)
                        {
                            return Err(format!("Duplicate animation name: '{}'", animation.name));
                        }

                        if script.poses.contains_key(&animation.name) {
                            return Err(format!(
                                "Name '{}' already used by a pose",
                                animation.name
                            ));
                        }

                        // Validate that all referenced poses exist
                        for statement in &animation.statements {
                            for pose_name in &statement.poses {
                                if !script.poses.contains_key(pose_name) {
                                    return Err(format!(
                                        "Animation '{}' references unknown pose '{}'",
                                        animation.name, pose_name
                                    ));
                                }
                            }
                        }

                        script.animations.insert(animation.name.clone(), animation);
                    }
                    BlockType::Main => {
                        let main = self.parse_main(&current_block)?;

                        // Validate that all referenced animations/poses exist
                        for reference in &main {
                            if !script.animations.contains_key(reference)
                                && !script.poses.contains_key(reference)
                            {
                                return Err(format!(
                                    "Main references unknown animation or pose '{}'",
                                    reference
                                ));
                            }
                        }

                        script.main = main;
                    }
                    BlockType::None => {}
                }
                current_block.clear();
                in_block = false;
                block_type = BlockType::None;
            }
        }

        if in_block {
            return Err("Unclosed block".to_string());
        }

        Ok(script.to_key_frames())
    }

    fn parse_pose(&self, text: &str) -> Result<MPLPose, String> {
        let mut pose_name = String::new();
        let mut statements = Vec::new();

        for (line_number, line) in text.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed == "{" || trimmed == "}" {
                continue;
            }

            if trimmed.starts_with("@pose") {
                let name_part = trimmed.trim_end_matches('{').trim();
                pose_name = name_part
                    .split_whitespace()
                    .nth(1)
                    .ok_or(format!("Line {}: Missing pose name", line_number + 1))?
                    .to_string();
                continue;
            }

            if trimmed.ends_with(';') {
                let stmt_text = trimmed.trim_end_matches(';').trim();
                if !stmt_text.is_empty() {
                    match crate::pose::MPLPoseStatement::from_str(stmt_text) {
                        Ok(stmt) => statements.push(stmt),
                        Err(e) => return Err(format!("Line {}: {}", line_number + 1, e)),
                    }
                }
            } else {
                return Err(format!(
                    "Line {}: Statement must end with semicolon",
                    line_number + 1
                ));
            }
        }

        if pose_name.is_empty() {
            return Err("No pose declaration found".to_string());
        }

        if statements.is_empty() {
            return Err("Pose must contain at least one statement".to_string());
        }

        Ok(MPLPose::new(pose_name, statements))
    }

    fn parse_animation(&self, text: &str) -> Result<MPLAnimation, String> {
        let mut animation_name = String::new();
        let mut statements = Vec::new();

        for (line_number, line) in text.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed == "{" || trimmed == "}" {
                continue;
            }

            if trimmed.starts_with("@animation") {
                let name_part = trimmed.trim_end_matches('{').trim();
                animation_name = name_part
                    .split_whitespace()
                    .nth(1)
                    .ok_or(format!("Line {}: Missing animation name", line_number + 1))?
                    .to_string();
                continue;
            }

            if trimmed.ends_with(';') {
                let stmt_text = trimmed.trim_end_matches(';').trim();
                if !stmt_text.is_empty() {
                    match crate::animation::MPLAnimationStatement::from_str(stmt_text) {
                        Ok(stmt) => statements.push(stmt),
                        Err(e) => return Err(format!("Line {}: {}", line_number + 1, e)),
                    }
                }
            } else {
                return Err(format!(
                    "Line {}: Statement must end with semicolon",
                    line_number + 1
                ));
            }
        }

        if animation_name.is_empty() {
            return Err("No animation declaration found".to_string());
        }

        if statements.is_empty() {
            return Err("Animation must contain at least one statement".to_string());
        }

        Ok(MPLAnimation::new(animation_name, statements))
    }

    fn parse_main(&self, text: &str) -> Result<Vec<String>, String> {
        let mut animations = Vec::new();

        for (line_number, line) in text.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed == "{" || trimmed == "}" {
                continue;
            }

            // Skip main declaration
            if trimmed == "main" || trimmed.starts_with("main") {
                continue;
            }

            // Parse animation/pose reference (must end with semicolon)
            if trimmed.ends_with(';') {
                let name = trimmed.trim_end_matches(';').trim();
                if !name.is_empty() {
                    animations.push(name.to_string());
                }
            } else {
                return Err(format!(
                    "Line {}: Animation reference must end with semicolon",
                    line_number + 1
                ));
            }
        }

        if animations.is_empty() {
            return Err("Main block must contain at least one animation reference".to_string());
        }

        Ok(animations)
    }
}