aada 0.1.1

纯 Rust 的 2D 骨骼动画引擎,兼容 Spine 4.x 导出格式(JSON + atlas)。求解到分层 Mesh 与各 Mesh 渲染设置为止,之后交给渲染器。
Documentation
//! 皮肤 / 附件(attachment):逻辑名到具体内容的映射。
//!
//! spine 语义:attachment 的唯一标识是 (slot, 附件逻辑名),解析期把逻辑名 intern 为
//! [`AttachmentNameIdx`],皮肤与动画附件帧全部改用索引,运行时零字符串查找。
//! 多皮肤按优先级列表采样([`SkinSet::sample`]),动画 attachment 切换覆盖皮肤选择。

use std::collections::HashMap;

use crate::{
    custom_index::{AsIndex, NameMap},
    data::runtime::{
        mesh::MeshIdx,
        slot::{SlotIdx, SlotSet},
    },
    new_index,
};

new_index!(SkinIdx, AttachmentNameIdx, AttachmentDataIdx);

/// 附件逻辑名 intern 表(解析期构建)
///
/// 附件逻辑名是 slot 命名空间的(不同 slot 可同名,同一 slot 内名字唯一)。
/// 解析期把 (slot, 逻辑名) 映射为类型化索引 [AttachmentNameIdx],
/// 皮肤、slot 默认附件、动画 attachment 帧全部改用索引,运行时零字符串查找。
#[derive(Debug, Clone)]
pub struct AttachmentNames {
    per_slot: Vec<HashMap<String, AttachmentNameIdx>>,
}

impl AttachmentNames {
    pub fn new(slot_count: usize) -> Self {
        Self {
            per_slot: (0..slot_count).map(|_| HashMap::new()).collect(),
        }
    }
    /// 获取 (slot, 逻辑名) 的类型化索引,首次出现时分配新索引(同一 slot 内按名字去重)
    pub fn intern(&mut self, slot_idx: SlotIdx, name: &str) -> AttachmentNameIdx {
        let m = &mut self.per_slot[slot_idx.as_index()];
        if let Some(&idx) = m.get(name) {
            return idx;
        }
        let idx = unsafe { AttachmentNameIdx::from_index(m.len()) };
        m.insert(name.to_string(), idx);
        idx
    }
}

/// 一个具体 attachment 的内容(静态不变)
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[derive(Debug, Clone, PartialEq)]
pub struct AttachmentData {
    pub name: String,
    /// 对应的网格,None 表示该 attachment 无网格(如 boundingbox 等)
    pub mesh: Option<MeshIdx>,
    /// attachment 默认颜色(RGBA 0-1):与 slot 颜色相乘构成最终 tint(乘性继承)
    pub color: [f32; 4],
}

/// 具体 attachment 集合(静态不变),按名称索引
#[derive(Debug, Clone)]
pub struct AttachmentSet {
    pub data: NameMap<AttachmentDataIdx, AttachmentData>,
}

impl Default for AttachmentSet {
    fn default() -> Self {
        Self::new()
    }
}

impl AttachmentSet {
    pub fn new() -> Self {
        Self {
            data: NameMap::new(),
        }
    }
    pub fn insert(&mut self, name: impl Into<String>, data: AttachmentData) -> AttachmentDataIdx {
        self.data.insert(name.into(), data)
    }
    pub fn get_index(&self, name: &str) -> Option<AttachmentDataIdx> {
        self.data.get_index(name)
    }
    pub fn get_mesh(&self, idx: AttachmentDataIdx) -> Option<MeshIdx> {
        self.data[idx].mesh
    }
}

/// 一张皮肤:slot 逻辑附件 -> 具体 attachment(静态不变)
///
/// spine 语义:attachment 的唯一标识是 (slot, 附件逻辑名),不同 slot 可有同名附件
/// (如 Sleeve_L_1_Front / Sleeve_L_1_Front_Grab 槽的附件都叫 Sleeve_L_1_Front)。
/// 解析期已把逻辑名映射为类型化索引 [AttachmentNameIdx],运行时按 (SlotIdx, AttachmentNameIdx) 直接查表
#[derive(Debug, Clone)]
pub struct Skin {
    /// slot 索引 -> (附件逻辑名索引 -> 具体 attachment),按 slot 索引的 Vec,运行时零字符串查找
    pub by_slot: Vec<HashMap<AttachmentNameIdx, AttachmentDataIdx>>,
}

impl Default for Skin {
    fn default() -> Self {
        Self::new()
    }
}

impl Skin {
    pub fn new() -> Self {
        Self {
            by_slot: Vec::new(),
        }
    }
    fn ensure_slot(&mut self, slot_idx: SlotIdx) {
        let n = slot_idx.as_index() + 1;
        if self.by_slot.len() < n {
            self.by_slot.resize_with(n, HashMap::new);
        }
    }
    pub fn insert(
        &mut self,
        slot_idx: SlotIdx,
        name_idx: AttachmentNameIdx,
        attachment: AttachmentDataIdx,
    ) {
        self.ensure_slot(slot_idx);
        self.by_slot[slot_idx.as_index()].insert(name_idx, attachment);
    }
    pub fn get(&self, slot_idx: SlotIdx, name_idx: AttachmentNameIdx) -> Option<AttachmentDataIdx> {
        self.by_slot
            .get(slot_idx.as_index())
            .and_then(|m| m.get(&name_idx))
            .copied()
    }
}

/// 皮肤集合(静态不变),通过名称一次性查索引
#[derive(Debug, Clone)]
pub struct SkinSet {
    pub data: NameMap<SkinIdx, Skin>,
}

impl Default for SkinSet {
    fn default() -> Self {
        Self::new()
    }
}

impl SkinSet {
    pub fn new() -> Self {
        Self {
            data: NameMap::new(),
        }
    }
    pub fn insert(&mut self, name: impl Into<String>, skin: Skin) -> SkinIdx {
        self.data.insert(name.into(), skin)
    }
    pub fn get_index(&self, name: &str) -> Option<SkinIdx> {
        self.data.get_index(name)
    }
    /// 采样皮肤设置:对每个 slot,按 skin 列表顺序查找其默认 attachment
    ///
    /// skins 按优先级从高到低排列,先命中的生效
    pub fn sample(
        &self,
        skins: &[SkinIdx],
        slots: &SlotSet,
        attachment_set: &AttachmentSet,
    ) -> AttachmentMap {
        let mut per_slot = Vec::with_capacity(slots.data.len());
        for slot_idx in slots.data.indexs() {
            let mut found = None;
            if let Some(name_idx) = slots.data[slot_idx].default_attachment_idx {
                for &skin_idx in skins {
                    if let Some(attachment) = self.data[skin_idx].get(slot_idx, name_idx) {
                        // 仅当 attachment 实际存在(有网格)时视为命中
                        if attachment_set.data[attachment].mesh.is_some() {
                            found = Some(attachment);
                            break;
                        }
                    }
                }
            }
            per_slot.push(found);
        }
        AttachmentMap { per_slot }
    }
}

/// 皮肤采样结果:每个 slot 选择的具体 attachment(静态不变)
#[derive(Debug, Clone, Default)]
pub struct AttachmentMap {
    per_slot: Vec<Option<AttachmentDataIdx>>,
}

impl AttachmentMap {
    pub fn get(&self, slot: SlotIdx) -> Option<AttachmentDataIdx> {
        use crate::custom_index::AsIndex;
        self.per_slot.get(slot.as_index()).copied().flatten()
    }
    /// 动画覆盖:设置某个 slot 当前的 attachment(None 表示隐藏该 slot)
    pub fn set(&mut self, slot: SlotIdx, attachment: Option<AttachmentDataIdx>) {
        use crate::custom_index::AsIndex;
        if let Some(v) = self.per_slot.get_mut(slot.as_index()) {
            *v = attachment;
        } else {
            let idx = slot.as_index();
            self.per_slot.resize(idx + 1, None);
            self.per_slot[idx] = attachment;
        }
    }
    pub fn iter(&self) -> impl Iterator<Item = (SlotIdx, Option<AttachmentDataIdx>)> {
        use crate::custom_index::AsIndex;
        self.per_slot
            .iter()
            .copied()
            .enumerate()
            .map(|(i, a)| (unsafe { SlotIdx::from_index(i) }, a))
    }
    /// 按绘制顺序(setup slot 索引排列)返回附件列表,越界索引跳过
    pub fn iter_with_order(&self, order: &[i32]) -> Vec<(SlotIdx, Option<AttachmentDataIdx>)> {
        use crate::custom_index::AsIndex;
        order
            .iter()
            .filter_map(|&i| {
                let i = i as usize;
                self.per_slot
                    .get(i)
                    .copied()
                    .map(|a| (unsafe { SlotIdx::from_index(i) }, a))
            })
            .collect()
    }
}