aada 0.1.1

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

use serde::{Deserialize, Serialize};

use crate::{
    data::runtime::{
        constraint::{ConstraintIdx, ConstraintSet, IkConstraintData, TransformConstraintData},
        skeleton::Skeleton,
    },
    error::Error,
};

/// 骨架级 IK 约束定义(序列化形式)
/// 字段名与 spine JSON 对齐(camelCase,直接映射),非 Rust 命名风格,故允许
#[allow(non_snake_case)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct IkConstraintDataStore {
    pub name: String,
    #[serde(default)]
    pub order: u32,
    #[serde(default)]
    pub bones: Vec<String>,
    pub target: String,
    #[serde(default)]
    pub mix: Option<f32>,
    #[serde(default)]
    pub bendPositive: Option<bool>,
    #[serde(default)]
    pub compress: Option<bool>,
    #[serde(default)]
    pub stretch: Option<bool>,
}

/// 骨架级 transform 约束定义(序列化形式)
/// 字段名与 spine JSON 对齐(camelCase,直接映射),非 Rust 命名风格,故允许
#[allow(non_snake_case)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TransformConstraintDataStore {
    pub name: String,
    #[serde(default)]
    pub order: u32,
    #[serde(default)]
    pub bones: Vec<String>,
    pub target: String,
    #[serde(default)]
    pub mixRotate: Option<f32>,
    #[serde(default)]
    pub mixX: Option<f32>,
    #[serde(default)]
    pub mixY: Option<f32>,
    #[serde(default)]
    pub mixScaleX: Option<f32>,
    #[serde(default)]
    pub mixScaleY: Option<f32>,
    #[serde(default)]
    pub mixShearY: Option<f32>,
    #[serde(default)]
    pub rotation: Option<f32>,
    #[serde(default)]
    pub x: Option<f32>,
    #[serde(default)]
    pub y: Option<f32>,
    #[serde(default)]
    pub scaleX: Option<f32>,
    #[serde(default)]
    pub scaleY: Option<f32>,
    #[serde(default)]
    pub shearY: Option<f32>,
    #[serde(default)]
    pub local: Option<bool>,
    #[serde(default)]
    pub relative: Option<bool>,
}

impl IkConstraintDataStore {
    pub fn to_rt(self, skeleton: &Skeleton) -> Result<IkConstraintData, Error> {
        let mut bones = Vec::with_capacity(self.bones.len());
        for b in &self.bones {
            bones.push(skeleton.get_bone_index(b).ok_or_else(|| {
                Error::InvalidData(format!("IK constraint references unknown bone: {b}"))
            })?);
        }
        let target = skeleton.get_bone_index(&self.target).ok_or_else(|| {
            Error::InvalidData(format!(
                "IK constraint '{}' references unknown target bone: {}",
                self.name, self.target
            ))
        })?;
        Ok(IkConstraintData {
            bones,
            target,
            mix: self.mix.unwrap_or(1.0),
            // 官方 4.2 SkeletonJson:bendPositive 缺省 true(bendDirection = +1)
            bend_positive: self.bendPositive.unwrap_or(true),
            compress: self.compress.unwrap_or(false),
            stretch: self.stretch.unwrap_or(false),
            order: self.order,
        })
    }
}

impl TransformConstraintDataStore {
    pub fn to_rt(self, skeleton: &Skeleton) -> Result<TransformConstraintData, Error> {
        let mut bones = Vec::with_capacity(self.bones.len());
        for b in &self.bones {
            bones.push(skeleton.get_bone_index(b).ok_or_else(|| {
                Error::InvalidData(format!("transform constraint references unknown bone: {b}"))
            })?);
        }
        let target = skeleton.get_bone_index(&self.target).ok_or_else(|| {
            Error::InvalidData(format!(
                "transform constraint '{}' references unknown target bone: {}",
                self.name, self.target
            ))
        })?;
        Ok(TransformConstraintData {
            bones,
            target,
            mix_rotate: self.mixRotate.unwrap_or(1.0),
            mix_x: self.mixX.unwrap_or(1.0),
            // 官方 4.2 SkeletonJson:mixY 缺省 = mixX
            mix_y: self.mixY.or(self.mixX).unwrap_or(1.0),
            mix_scale_x: self.mixScaleX.unwrap_or(1.0),
            // 官方:mixScaleY 缺省 = mixScaleX
            mix_scale_y: self.mixScaleY.or(self.mixScaleX).unwrap_or(1.0),
            mix_shear_y: self.mixShearY.unwrap_or(1.0),
            offset_rotation: self.rotation.unwrap_or(0.0),
            offset_x: self.x.unwrap_or(0.0),
            offset_y: self.y.unwrap_or(0.0),
            // 官方:offsetScale 缺省 0(加法混合基量)
            offset_scale_x: self.scaleX.unwrap_or(0.0),
            offset_scale_y: self.scaleY.unwrap_or(0.0),
            offset_shear_y: self.shearY.unwrap_or(0.0),
            order: self.order,
        })
    }
}

/// 骨架级约束集合(序列化形式)
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConstraintDataStore {
    #[serde(default)]
    pub ik: Vec<IkConstraintDataStore>,
    #[serde(default)]
    pub transform: Vec<TransformConstraintDataStore>,
}

impl ConstraintDataStore {
    /// 转换到运行时约束集合,并按 order 全局排序
    pub fn to_rt(self, skeleton: &Skeleton) -> Result<ConstraintSet, Error> {
        let mut ik = Vec::with_capacity(self.ik.len());
        for c in self.ik {
            ik.push(c.to_rt(skeleton)?);
        }
        let mut transform = Vec::with_capacity(self.transform.len());
        for c in self.transform {
            transform.push(c.to_rt(skeleton)?);
        }
        let mut order: Vec<(u32, ConstraintIdx)> = Vec::with_capacity(ik.len() + transform.len());
        for (i, c) in ik.iter().enumerate() {
            order.push((c.order, ConstraintIdx::Ik(i as u32)));
        }
        for (i, c) in transform.iter().enumerate() {
            order.push((c.order, ConstraintIdx::Transform(i as u32)));
        }
        order.sort_by_key(|(o, _)| *o);
        Ok(ConstraintSet {
            ik,
            transform,
            order: order.into_iter().map(|(_, i)| i).collect(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn parse_ik_constraint_json() {
        let v = json!({
            "name": "Arm_L_Grab_IK",
            "order": 5,
            "bones": ["Upperarm_L_Grab", "Arm_L_2_Grab"],
            "target": "Arm_L_Grab_IK",
            "mix": 0,
            "bendPositive": false
        });
        let store: IkConstraintDataStore = serde_json::from_value(v).unwrap();
        assert_eq!(store.name, "Arm_L_Grab_IK");
        assert_eq!(store.bones.len(), 2);
        assert_eq!(store.mix, Some(0.0));
        assert_eq!(store.bendPositive, Some(false));
        assert_eq!(store.order, 5);
    }

    #[test]
    fn parse_transform_constraint_json() {
        let v = json!({
            "name": "Foot_L_T",
            "order": 8,
            "bones": ["Foot_L"],
            "target": "Leg_L_IK",
            "mixScaleX": 0,
            "mixShearY": 0,
            "rotation": -87.5
        });
        let store: TransformConstraintDataStore = serde_json::from_value(v).unwrap();
        assert_eq!(store.mixScaleX, Some(0.0));
        // 未指定字段保持默认混合
        assert_eq!(store.mixRotate, None);
        assert_eq!(store.rotation, Some(-87.5));
    }
}