aada 0.1.0

纯 Rust 的 2D 骨骼动画引擎,兼容 Spine 4.x 导出格式(JSON + atlas)。求解到分层 Mesh 与各 Mesh 渲染设置为止,之后交给渲染器。
Documentation
# aada

纯 Rust 的 2D 骨骼动画引擎库,兼容 [Spine](https://esotericsoftware.com/spine) 4.x 导出格式(JSON + atlas)。只负责求解到"分层 Mesh + 各 Mesh 渲染设置"为止,之后的绘制交给渲染器。无渲染器依赖,仅依赖 `glam` / `serde`。

## 引入

```toml
[dependencies]
aada = "0.1"
```

默认不启用任何可选 feature。可选 `bevy_reflect` 使核心类型可被反射,供 bevy 等编辑器/调试工具直接检查与编辑:

```toml
aada = { version = "0.1", features = ["bevy_reflect"] }
```

## 快速开始

引擎数据从两个文件构建:`xxx.json`(骨架/皮肤/动画)+ `xxx.atlas`(图集布局),外加对应的图集纹理图片。

```rust
use aada::prelude::*;

// 1. 构建运行时(spine JSON + atlas 文本)-> (RT, 图集页列表)
let json = std::fs::read_to_string("assets/hero.skel.json")?;
let atlas_text = std::fs::read_to_string("assets/hero.atlas.txt")?;
let (rt, _pages) = build_rt(&json, &atlas_text)?;

// 2. 按名称取索引(之后全走索引,运行时零字符串查找)
let anim = rt.animates.get_index("walk").unwrap();
let skin = rt.skins.get_index("default").unwrap();

// 3. 每帧走分步管线(config 跨帧复用,连续时间采样 O(1) 加速),
//    中间可插入自定义逻辑(约束前操纵骨骼、覆盖约束参数)
let mut config = AnimateConfig::new();
for t in 0..60 {
    let sampled = rt.sample(anim, &mut config, t as f32 / 60.0);
    let mut pose = rt.pose_from_sample(&sampled); // 可在此修改 pose(约束前骨骼操纵)
    let state = rt.constraint_state(&sampled); // 可在此覆盖约束参数
    rt.apply_constraint_state(&mut pose, &state);
    let payloads = rt.build_payloads(&[skin], &sampled, &mut pose);
    // payloads: Vec<MeshPayload>,顺序即渲染顺序(靠后覆盖靠前)
}
```

无需在管线中间插入自定义逻辑时,`RT::render_frame` 提供一键式入口(内部即上述五步的顺序拼接):

```rust
let payloads = rt.render_frame(&[skin], anim, &mut config, t);
```

## 渲染负载

`Vec<MeshPayload>` 的顺序即渲染顺序,渲染层按序绘制即可。`MeshPayload` 字段:

| 字段 | 说明 |
|------|------|
| `usage` | `Mesh(idx)` / `ClipStart{id}` / `ClipEnd{id}` |
| `slot` | 所属 slot 索引 |
| `positions` | 模型空间顶点(蒙皮变形后) |
| `uvs` | 图集局部 UV |
| `indices` | 三角形索引 |
| `tint_color` | 乘性顶点色(slot 颜色 × attachment 颜色,已含动画覆盖) |
| `uv_transform` | UV 变换矩阵(当前恒为单位矩阵,预留) |

同一 attachment 全局唯一 `mesh_idx`,渲染层可用它识别网格是否切换。

## 软件裁剪与位图渲染

`Vec<MeshPayload>` 直接实现 `Clip` 和 `Render` trait(无窗口渲染 / 离线导出):

```rust
use aada::data::runtime::clip::{AsBitmap, AsTexture, Clip, Render};

let mut payloads = rt.render_frame(&[skin], anim, &mut config, t);
payloads.clip_software(); // 消费 ClipStart/ClipEnd,把网格裁到多边形内(嵌套取交集)

// 光栅化到位图:half_extent 为每轴半范围(可不等长宽,如 Vec2::new(400.0, 200.0) 表示
// 以 offset 为中心、模型空间里 800×400 的矩形区域会被拉伸到整张 W×H 位图);
// offset 为视口中心在模型空间的坐标;
// 纹理采样与像素输出用闭包直接实现(functional_trait 宏)
let half_extent = Vec2::splat(400.0);
let mut px = vec![[0.0f32; 4]; W * H];
payloads.render_bitmap(W, H, half_extent, center, Some(&tex_closure), &mut |x, y, rgba| {
    px[y * W + x] = rgba;
});
// 或逐像素查询:let c = payloads.render_pixel(x, y, tex);
// 包围盒:      let (min, max) = payloads.aabb().unwrap();

// 输出剪影位图:Render::render_tint 不采样纹理,以 tint 纯色填充,配合 utils::write_bmp_rgba 写出 .bmp
let mut silhouette = vec![[0.0f32; 4]; W * H];
payloads.render_tint(W, H, half_extent, center, &mut |x, y, rgba| {
    silhouette[y * W + x] = rgba;
});
let rgba: Vec<u8> = silhouette
    .iter()
    .flat_map(|c| c.iter().map(|v| (v * 255.0).round() as u8))
    .collect();
let bmp = aada::utils::write_bmp_rgba(W as u32, H as u32, &rgba);
std::fs::write("silhouette.bmp", bmp)?;
```

## 动画支持矩阵

| 能力 | 状态 |
|------|------|
| 骨骼通道(translate/rotate/scale/shear,单轴/双轴拆分) | ✅ |
| 曲线:linear / stepped / bezier(构建期预采样 + 运行时查表) | ✅ |
| slot 颜色(RGBA 逐分量独立曲线插值) | ✅ |
| slot attachment 切换(step 语义,支持隐藏) | ✅ |
| 约束时间轴(ik / transform 参数覆盖) | ✅ |
| deform 顶点变形(加权 / 非加权) | ✅ |
| draw order 绘制顺序 | ✅ |
| 裁剪 attachment(clipping)+ 软件裁剪 | ✅ |
| IK 约束(1/2 骨骼链,spine 4.2 移植)与 transform 约束(world 绝对模式) | ✅ |
| 多动画混合 / crossfade | ❌(单动画采样,可多次调用自行混合) |
| events / path 约束 / sequence 附件 | ❌ |

采样语义遵循官方 spine(有测试对照):t 在时间轴首帧之前时间轴未生效(骨骼保持 setup、slot 保持默认),t 超出末帧后取末帧值。

## 已知限制

- 只支持 spine 4.x JSON(`.skel` 二进制、3.x 旧格式不支持);atlas 只认 4.x `bounds/offsets` 字段。
- 单纹理设计:所有 `MeshPayload` 共享同一纹理;多页 atlas 需渲染层自行处理(本库只输出 `AtlasPage` 列表)。
- `linkedmesh` / `boundingbox` / 字体附件解析时静默跳过。

## License

MIT OR Apache-2.0