aada 0.1.1

纯 Rust 的 2D 骨骼动画引擎,兼容 Spine 4.x 导出格式(JSON + atlas)。求解到分层 Mesh 与各 Mesh 渲染设置为止,之后交给渲染器。
Documentation
//! 骨骼的序列化中间结构(store 层),可转换为运行时 [`Skeleton`]。

use serde::{Deserialize, Serialize};

use crate::{
    data::runtime::skeleton::{BoneDataStatic, InheritMode, ShearMode, Skeleton},
    error::Error,
    utils::OptionMoveTo,
};

/// 静态骨骼数据(不包含动画,仅描述骨骼层级和初始变换)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BoneDataStore {
    /// 骨骼名称(唯一标识符)
    pub name: String,
    /// 骨骼在局部空间中的平移(相对于父骨骼)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub translate_x: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub translate_y: Option<f32>,

    /// 局部旋转(单位:度,正值 = 逆时针)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rotation_deg: Option<f32>,

    /// 局部缩放
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scale_x: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scale_y: Option<f32>,

    /// 剪切角度(单位:度)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shear_x_deg: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shear_y_deg: Option<f32>,

    /// 骨骼长度(沿局部 X 轴),用于 IK / Path / Physics 等约束
    #[serde(skip_serializing_if = "Option::is_none")]
    pub length: Option<f32>,

    /// 变换继承模式(控制如何从父骨骼继承变换)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub inherit_mode: Option<InheritMode>,

    /// 是否默认激活(false 表示仅当皮肤包含时才启用)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_active: Option<bool>,

    /// 父骨骼名称(用于构建骨骼层级)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent: Option<String>,

    /// 调试显示颜色(十六进制,RRGGBBAA,与 CSS 颜色格式顺序一致),仅用于编辑器调试显示
    #[serde(skip_serializing_if = "Option::is_none")]
    pub debug_color: Option<String>,

    /// 剪切模式
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shear_mode: Option<ShearMode>,
}

#[test]
fn t_serde() {
    let b = BoneDataStore::default();
    let s = serde_json::to_string(&b).unwrap();
    println!("{}", s);
    assert_eq!(s, r#"{"name":""}"#);
}

impl Skeleton {
    /// 从静态骨骼数据列表创建骨骼,注意子骨骼必须在父骨骼之后出现
    /// 颜色解析错误会静默填充默认值
    pub fn from_bone_data_list(bones: &[BoneDataStore]) -> Result<Self, Error> {
        Self::from_bone_data_list_config(bones, true)
    }
    /// 从静态骨骼数据列表创建骨骼,注意子骨骼必须在父骨骼之后出现
    pub fn from_bone_data_list_config(
        bones: &[BoneDataStore],
        ignore_color_err: bool,
    ) -> Result<Self, Error> {
        let mut skeleton = Skeleton::new();
        for bone in bones {
            if skeleton.get_bone_index(&bone.name).is_some() {
                return Err(Error::InvalidData(format!(
                    "Duplicate bone name: {}",
                    bone.name
                )));
            }
            let parent_idx = match &bone.parent {
                Some(pname) => Some(skeleton.get_bone_index(pname).ok_or_else(|| {
                    Error::InvalidData(format!(
                        "Parent bone not found: processing {}, parent {}",
                        bone.name, pname
                    ))
                })?),
                None => None,
            };
            let mut bone_rt = BoneDataStatic::default();
            bone.translate_x.write_to(&mut bone_rt.translate_x);
            bone.translate_y.write_to(&mut bone_rt.translate_y);
            bone.rotation_deg.write_to(&mut bone_rt.rotation_deg);
            bone.scale_x.write_to(&mut bone_rt.scale_x);
            bone.scale_y.write_to(&mut bone_rt.scale_y);
            bone.shear_x_deg.write_to(&mut bone_rt.shear_x_deg);
            bone.shear_y_deg.write_to(&mut bone_rt.shear_y_deg);
            bone.length.write_to(&mut bone_rt.length);
            bone.inherit_mode.write_to(&mut bone_rt.inherit_mode);
            bone.shear_mode.write_to(&mut bone_rt.shear_mode);
            bone.default_active.write_to(&mut bone_rt.default_active);
            if let Some(color) = bone.debug_color.as_ref() {
                if ignore_color_err {
                    if let Ok(c) = rrggbbaa_to_4f32(color) {
                        bone_rt.debug_color = c;
                    }
                } else {
                    bone_rt.debug_color = rrggbbaa_to_4f32(color)?;
                }
            }
            bone_rt.parent_index = parent_idx;
            skeleton.insert_bone(&bone.name, bone_rt)?;
        }
        skeleton.build_children_map();
        skeleton.build_roots();
        Ok(skeleton)
    }
}

fn rrggbbaa_to_4f32(rrggbbaa: &str) -> Result<[f32; 4], Error> {
    if rrggbbaa.len() != 8 {
        return Err(Error::InvalidData(format!(
            "Invalid color format: {}",
            rrggbbaa
        )));
    }
    let r = u8::from_str_radix(&rrggbbaa[0..2], 16)
        .map_err(|_| Error::InvalidData(format!("Invalid color format in r: {}", rrggbbaa)))?;
    let g = u8::from_str_radix(&rrggbbaa[2..4], 16)
        .map_err(|_| Error::InvalidData(format!("Invalid color format in g: {}", rrggbbaa)))?;
    let b = u8::from_str_radix(&rrggbbaa[4..6], 16)
        .map_err(|_| Error::InvalidData(format!("Invalid color format in b: {}", rrggbbaa)))?;
    let a = u8::from_str_radix(&rrggbbaa[6..8], 16)
        .map_err(|_| Error::InvalidData(format!("Invalid color format in a: {}", rrggbbaa)))?;
    Ok([
        r as f32 / 255.0,
        g as f32 / 255.0,
        b as f32 / 255.0,
        a as f32 / 255.0,
    ])
}
#[test]
fn test_impl_skeleton() {
    let bones = vec![
        BoneDataStore {
            name: "root".to_string(),
            translate_x: Some(10.0),
            translate_y: Some(20.0),
            ..Default::default()
        },
        BoneDataStore {
            name: "child".to_string(),
            parent: Some("root".to_string()),
            rotation_deg: Some(45.0),
            ..Default::default()
        },
    ];
    let skeleton = Skeleton::from_bone_data_list(&bones).unwrap();
    assert_eq!(skeleton.data_static.len(), 2);
    let root_idx = skeleton.get_bone_index("root").unwrap();
    let child_idx = skeleton.get_bone_index("child").unwrap();
    assert_eq!(skeleton.data_static[child_idx].parent_index, Some(root_idx));
    assert_eq!(skeleton.data_static[root_idx].translate_x, 10.0);
    assert_eq!(skeleton.data_static[root_idx].translate_y, 20.0);
}

#[test]
fn test_skeleton_build_fail() {
    // 测试找不到父骨骼时报错
    {
        let bones = vec![
            BoneDataStore {
                name: "root".to_string(),
                translate_x: Some(10.0),
                translate_y: Some(20.0),
                ..Default::default()
            },
            BoneDataStore {
                name: "child".to_string(),
                parent: Some("root1".to_string()),
                rotation_deg: Some(45.0),
                ..Default::default()
            },
        ];
        let skeleton = Skeleton::from_bone_data_list(&bones);
        assert_eq!(
            skeleton.unwrap_err(),
            Error::InvalidData("Parent bone not found: processing child, parent root1".to_string())
        );
    }
    // 测试重复骨骼名称时报错
    {
        let bones = vec![
            BoneDataStore {
                name: "root".to_string(),
                ..Default::default()
            },
            BoneDataStore {
                name: "root".to_string(),
                ..Default::default()
            },
        ];
        let skeleton = Skeleton::from_bone_data_list(&bones);
        assert_eq!(
            skeleton.unwrap_err(),
            Error::InvalidData("Duplicate bone name: root".to_string())
        );
    }
    // 测试父骨骼在子骨骼后
    {
        let bones = vec![
            BoneDataStore {
                name: "child".to_string(),
                parent: Some("root".to_string()),
                ..Default::default()
            },
            BoneDataStore {
                name: "root".to_string(),
                ..Default::default()
            },
        ];
        let skeleton = Skeleton::from_bone_data_list(&bones);
        assert_eq!(
            skeleton.unwrap_err(),
            Error::InvalidData("Parent bone not found: processing child, parent root".to_string())
        );
    }
}
#[test]
fn test_manual_construct() {
    let skeleton_manual = {
        let mut skeleton = Skeleton::new();
        let root = {
            let bone = BoneDataStatic {
                length: 50.0,
                debug_color: [1.0, 0.0, 0.0, 1.0],
                ..Default::default()
            };
            skeleton.insert_bone("root", bone).unwrap()
        };
        let bone1 = {
            let bone = BoneDataStatic {
                length: 60.0,
                parent_index: Some(root),
                translate_x: 50.0,
                debug_color: [0.0, 1.0, 0.0, 1.0],
                ..Default::default()
            };
            skeleton.insert_bone("bone1", bone).unwrap()
        };
        let _bone2 = {
            let bone = BoneDataStatic {
                length: 40.0,
                parent_index: Some(bone1),
                translate_x: 60.0,
                debug_color: [0.0, 0.0, 1.0, 1.0],
                default_active: false,
                ..Default::default()
            };
            skeleton.insert_bone("bone2", bone).unwrap()
        };
        skeleton.build_children_map();
        skeleton.build_roots();
        skeleton
    };
    let skeleton_from_data = {
        let bones = vec![
            BoneDataStore {
                name: "root".to_string(),
                length: Some(50.0),
                debug_color: Some("ff0000ff".to_string()),
                ..Default::default()
            },
            BoneDataStore {
                name: "bone1".to_string(),
                parent: Some("root".to_string()),
                translate_x: Some(50.0),
                length: Some(60.0),
                debug_color: Some("00ff00ff".to_string()),
                ..Default::default()
            },
            BoneDataStore {
                name: "bone2".to_string(),
                parent: Some("bone1".to_string()),
                translate_x: Some(60.0),
                length: Some(40.0),
                debug_color: Some("0000ffff".to_string()),
                default_active: Some(false),
                ..Default::default()
            },
        ];
        Skeleton::from_bone_data_list(&bones).unwrap()
    };
    assert_eq!(
        skeleton_manual.data_static.len(),
        skeleton_from_data.data_static.len()
    );
    skeleton_manual
        .data_static
        .iter()
        .zip(skeleton_from_data.data_static.iter())
        .for_each(|((idx_a, name_a, data_a), (idx_b, name_b, data_b))| {
            assert_eq!(idx_a, idx_b);
            assert_eq!(name_a, name_b);
            assert_eq!(data_a.translate_x, data_b.translate_x);
            assert_eq!(data_a.translate_y, data_b.translate_y);
            assert_eq!(data_a.rotation_deg, data_b.rotation_deg);
            assert_eq!(data_a.scale_x, data_b.scale_x);
            assert_eq!(data_a.scale_y, data_b.scale_y);
            assert_eq!(data_a.shear_x_deg, data_b.shear_x_deg);
            assert_eq!(data_a.shear_y_deg, data_b.shear_y_deg);
            assert_eq!(data_a.length, data_b.length);
            assert_eq!(data_a.inherit_mode, data_b.inherit_mode);
            assert_eq!(data_a.parent_index, data_b.parent_index);
            assert_eq!(data_a.debug_color, data_b.debug_color);
            assert_eq!(data_a.shear_mode, data_b.shear_mode);
            assert_eq!(data_a.default_active, data_b.default_active);
        });
    assert_eq!(skeleton_manual.children, skeleton_from_data.children);
    assert_eq!(skeleton_manual.roots, skeleton_from_data.roots);
}

#[test]
fn test_build_color_err() {
    let default_color = crate::data::runtime::skeleton::BoneDataStatic::default().debug_color;
    {
        let bones = vec![BoneDataStore {
            name: "root".to_string(),
            debug_color: Some("zzzzzzzz".to_string()),
            ..Default::default()
        }];
        let skeleton_fail = Skeleton::from_bone_data_list_config(&bones, false);
        assert_eq!(
            skeleton_fail.unwrap_err(),
            Error::InvalidData("Invalid color format in r: zzzzzzzz".to_string())
        );
        let skeleton_ignore = Skeleton::from_bone_data_list(&bones).unwrap();
        assert_eq!(
            skeleton_ignore.data_static[skeleton_ignore.get_bone_index("root").unwrap()]
                .debug_color,
            default_color
        );
    }
    {
        let bones = vec![BoneDataStore {
            name: "root".to_string(),
            debug_color: Some("ffffzzzz".to_string()),
            ..Default::default()
        }];
        let skeleton_fail = Skeleton::from_bone_data_list_config(&bones, false);
        assert_eq!(
            skeleton_fail.unwrap_err(),
            Error::InvalidData("Invalid color format in b: ffffzzzz".to_string())
        );
        let skeleton_ignore = Skeleton::from_bone_data_list(&bones).unwrap();
        assert_eq!(
            skeleton_ignore.data_static[skeleton_ignore.get_bone_index("root").unwrap()]
                .debug_color,
            default_color
        );
    }

    {
        let bones_success = vec![BoneDataStore {
            name: "root".to_string(),
            debug_color: Some("ff00ff00".to_string()),
            ..Default::default()
        }];
        let skeleton_success = Skeleton::from_bone_data_list_config(&bones_success, false).unwrap();
        let skeleton_success_ignore = Skeleton::from_bone_data_list(&bones_success).unwrap();
        assert_eq!(
            skeleton_success.data_static[skeleton_success.get_bone_index("root").unwrap()]
                .debug_color,
            skeleton_success_ignore.data_static
                [skeleton_success_ignore.get_bone_index("root").unwrap()]
            .debug_color
        );
        assert_eq!(
            skeleton_success.data_static[skeleton_success.get_bone_index("root").unwrap()]
                .debug_color,
            [1.0, 0.0, 1.0, 0.0]
        );
    }
}