spine2d 0.3.0

Pure Rust runtime for Spine 4.3 (unofficial)
Documentation
use crate::{Skeleton, SkeletonData};

const SKELETON_IK_TWO_BONES: &str = r#"
{
  "skeleton": { "spine": "4.3.00" },
  "bones": [
    { "name": "root" },
    { "name": "p", "parent": "root", "length": 1, "x": 0, "y": 0 },
    { "name": "c", "parent": "p", "length": 1, "x": 1, "y": 0 },
    { "name": "t", "parent": "root", "x": 1, "y": 1 }
  ],
  "slots": [],
  "skins": {},
  "ik": [
    { "name": "ik", "bones": ["p", "c"], "target": "t", "mix": 1, "bendPositive": true }
  ],
  "animations": {}
}
"#;

const SKELETON_IK_ONE_BONE: &str = r#"
{
  "skeleton": { "spine": "4.3.00" },
  "bones": [
    { "name": "root" },
    { "name": "p", "parent": "root", "length": 1, "x": 0, "y": 0 },
    { "name": "t", "parent": "root", "x": 0, "y": 1 }
  ],
  "slots": [],
  "skins": {},
  "ik": [
    { "name": "ik", "bones": ["p"], "target": "t", "mix": 1, "bendPositive": true }
  ],
  "animations": {}
}
"#;

const SKELETON_IK_DEFAULT_BEND_POSITIVE: &str = r#"
{
  "skeleton": { "spine": "4.3.00" },
  "bones": [
    { "name": "root" },
    { "name": "p", "parent": "root", "length": 1, "x": 0, "y": 0 },
    { "name": "t", "parent": "root", "x": 0, "y": 1 }
  ],
  "slots": [],
  "skins": {},
  "ik": [
    { "name": "ik", "bones": ["p"], "target": "t", "mix": 1 }
  ],
  "animations": {}
}
"#;

fn one_bone_stretch_json(scale_y_mode: Option<&str>) -> String {
    let scale_y = scale_y_mode
        .map(|mode| format!(r#", "scaleY": "{mode}""#))
        .unwrap_or_default();
    format!(
        r#"
{{
  "skeleton": {{ "spine": "4.3.00" }},
  "bones": [
    {{ "name": "root" }},
    {{ "name": "p", "parent": "root", "length": 1, "x": 0, "y": 0, "scaleY": 2 }},
    {{ "name": "t", "parent": "root", "x": 2, "y": 0 }}
  ],
  "slots": [],
  "skins": {{}},
  "ik": [
    {{ "name": "ik", "bones": ["p"], "target": "t", "mix": 1, "stretch": true{scale_y} }}
  ],
  "animations": {{}}
}}
"#
    )
}

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 ik_two_bones_moves_end_effector_close_to_target() {
    let data = SkeletonData::from_json_str(SKELETON_IK_TWO_BONES).unwrap();
    let mut skeleton = Skeleton::new(data);
    skeleton.set_to_setup_pose();
    skeleton.update_world_transform();

    let target = &skeleton.bones[3];
    let child = &skeleton.bones[2];
    let child_len = skeleton.data.bones[2].length;
    let tip_x = child.a * child_len + child.world_x;
    let tip_y = child.c * child_len + child.world_y;

    assert_approx(tip_x, target.world_x);
    assert_approx(tip_y, target.world_y);
}

#[test]
fn ik_one_bone_rotates_toward_target() {
    let data = SkeletonData::from_json_str(SKELETON_IK_ONE_BONE).unwrap();
    let mut skeleton = Skeleton::new(data);
    skeleton.set_to_setup_pose();
    skeleton.update_world_transform();

    let target = &skeleton.bones[2];
    let bone = &skeleton.bones[1];
    let bone_len = skeleton.data.bones[1].length;
    let tip_x = bone.a * bone_len + bone.world_x;
    let tip_y = bone.c * bone_len + bone.world_y;

    assert_approx(tip_x, target.world_x);
    assert_approx(tip_y, target.world_y);
}

#[test]
fn ik_constraint_bend_positive_defaults_to_true() {
    let data = SkeletonData::from_json_str(SKELETON_IK_DEFAULT_BEND_POSITIVE).unwrap();
    assert_eq!(data.ik_constraints.len(), 1);
    assert_eq!(data.ik_constraints[0].bend_direction, 1);
}

#[test]
fn ik_scale_y_mode_controls_stretch_scale_y() {
    let data = SkeletonData::from_json_str(&one_bone_stretch_json(None)).unwrap();
    let mut skeleton = Skeleton::new(data);
    skeleton.set_to_setup_pose();
    skeleton.update_world_transform();
    assert_approx(skeleton.bones[1].ascale_x, 2.0);
    assert_approx(skeleton.bones[1].ascale_y, 2.0);

    let data = SkeletonData::from_json_str(&one_bone_stretch_json(Some("uniform"))).unwrap();
    let mut skeleton = Skeleton::new(data);
    skeleton.set_to_setup_pose();
    skeleton.update_world_transform();
    assert_approx(skeleton.bones[1].ascale_x, 2.0);
    assert_approx(skeleton.bones[1].ascale_y, 4.0);
}