spine2d 0.4.0

Pure Rust runtime for Spine 4.3 (unofficial)
Documentation
use crate::runtime::MixBlend;
use crate::{Skeleton, SkeletonData, apply_animation};
use std::sync::Arc;

const SKELETON_WITH_SLIDER_TIMELINES: &str = r#"
{
  "skeleton": { "spine": "4.3.00" },
  "bones": [ { "name": "root" } ],
  "slots": [
    { "name": "slot0", "bone": "root", "attachment": "a" },
    { "name": "slot1", "bone": "root", "attachment": "b" }
  ],
  "skins": {
    "default": {
      "slot0": { "a": { "type": "region", "path": "a.png", "width": 10, "height": 10 } },
      "slot1": { "b": { "type": "region", "path": "b.png", "width": 10, "height": 10 } }
    }
  },
  "slider": [
    {
      "name": "slider",
      "time": 1.0,
      "mix": 0.5,
      "animation": "anim"
    }
  ],
  "animations": {
    "idle": {},
    "anim": {
      "drawOrder": [
        { "time": 0.0, "offsets": [ { "slot": "slot1", "offset": -1 } ] }
      ],
      "slider": {
        "slider": {
          "time": [
            { "time": 0.0, "value": 3.0 },
            { "time": 1.0, "value": 3.0 }
          ],
          "mix": [
            { "time": 0.0, "value": 0.9 },
            { "time": 1.0, "value": 0.9 }
          ]
        }
      }
    }
  }
}
"#;

fn assert_approx(actual: f32, expected: f32) {
    let diff = (actual - expected).abs();
    assert!(
        diff <= 1.0e-3,
        "expected {expected}, got {actual} (diff {diff})"
    );
}

#[test]
fn slider_timelines_update_runtime_values() {
    let data = SkeletonData::from_json_str(SKELETON_WITH_SLIDER_TIMELINES).unwrap();
    let anim = data.find_animation("anim").unwrap();
    let mut skeleton = Skeleton::new(data.clone());

    skeleton.setup_pose();
    apply_animation(anim, &mut skeleton, 0.5, false, 1.0, MixBlend::Replace);

    assert_approx(skeleton.slider_constraints[0].get_time(), 3.0);
    assert_approx(skeleton.slider_constraints[0].get_mix(), 0.9);
}

#[test]
fn slider_timelines_apply_negative_alpha_like_cpp() {
    let data = SkeletonData::from_json_str(SKELETON_WITH_SLIDER_TIMELINES).unwrap();
    let anim = data.find_animation("anim").unwrap();
    let mut skeleton = Skeleton::new(data.clone());

    skeleton.setup_pose();
    apply_animation(anim, &mut skeleton, 0.5, false, -0.5, MixBlend::Replace);

    assert_approx(skeleton.slider_constraints[0].get_time(), 0.0);
    assert_approx(skeleton.slider_constraints[0].get_mix(), 0.3);
}

#[test]
fn skeleton_data_find_slider_animations_appends_like_cpp() {
    let data = SkeletonData::from_json_str(SKELETON_WITH_SLIDER_TIMELINES).unwrap();
    let idle = data.find_animation("idle").unwrap();
    let mut animations = vec![idle];

    let returned_len = data.find_slider_animations(&mut animations).len();

    assert_eq!(returned_len, 2);
    assert_eq!(animations[0].name, "idle");
    assert_eq!(animations[1].name, "anim");
}

#[test]
fn slider_animation_name_only_binding_still_drives_runtime() {
    let mut data = SkeletonData::from_json_str(SKELETON_WITH_SLIDER_TIMELINES).unwrap();
    {
        let data = Arc::make_mut(&mut data);
        let slider = data
            .slider_constraints
            .get_mut(0)
            .expect("slider constraint exists");
        slider.animation = None;
        slider.animation_name = Some("anim".to_string());
    }

    let mut skeleton = Skeleton::new(Arc::clone(&data));
    assert_eq!(data.find_slider_animations(&mut Vec::new()).len(), 1);
    skeleton.setup_pose();
    skeleton.update_world_transform_with_physics(crate::Physics::None);
    assert_eq!(skeleton.get_draw_order(), &[1, 0]);
}