spine_runtime 0.2.2

Unofficial native Rust port of the Spine 4.3 runtime. Renderer-agnostic.
Documentation
// Spine Runtimes License Agreement
// Last updated April 5, 2025. Replaces all prior versions.
//
// Copyright (c) 2013-2025, Esoteric Software LLC
//
// Integration of the Spine Runtimes into software or otherwise creating
// derivative works of the Spine Runtimes is permitted under the terms and
// conditions of Section 2 of the Spine Editor License Agreement:
// http://esotericsoftware.com/spine-editor-license
//
// Otherwise, it is permitted to integrate the Spine Runtimes into software
// or otherwise create derivative works of the Spine Runtimes (collectively,
// "Products"), provided that each user of the Products must obtain their own
// Spine Editor license and redistribution of the Products in any form must
// include this license and copyright notice.
//
// THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
// BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

//! Prints one line per unbatched render command at the setup pose: vertex and
//! triangle counts, world-space bounds, and the slot and attachment it came
//! from. Commands collapsed to a point are flagged `!!`.
//!
//! `SPINE_RIG`, `SPINE_SKEL` and `SPINE_ATLAS` pick the export under
//! `../spine-runtimes/examples` (default spineboy):
//!
//! ```text
//! SPINE_RIG=dragon SPINE_SKEL=dragon-ess SPINE_ATLAS=dragon-pma \
//!     cargo run --example dump_slots
//! ```

use std::path::PathBuf;
use std::sync::Arc;

use spine_runtime::atlas::Atlas;
use spine_runtime::data::{Attachment, BoneData};
use spine_runtime::load::{AtlasAttachmentLoader, SkeletonBinary};
use spine_runtime::render::SkeletonRenderer;
use spine_runtime::skeleton::{Physics, Skeleton};

fn env_str(key: &str, default: &str) -> String {
    std::env::var(key).unwrap_or_else(|_| default.to_string())
}

fn main() {
    let rig = env_str("SPINE_RIG", "spineboy");
    let skel = env_str("SPINE_SKEL", "spineboy-pro");
    let atlas_stem = env_str("SPINE_ATLAS", "spineboy-pma");

    let dir: PathBuf = ["..", "spine-runtimes", "examples", &rig, "export"]
        .iter()
        .collect();
    let atlas_src = std::fs::read_to_string(dir.join(format!("{atlas_stem}.atlas"))).unwrap();
    let atlas = Atlas::parse(&atlas_src).unwrap();
    let mut loader = AtlasAttachmentLoader::new(&atlas);
    let bytes = std::fs::read(dir.join(format!("{skel}.skel"))).unwrap();
    let data = SkeletonBinary::with_loader(&mut loader)
        .read(&bytes)
        .unwrap();
    let data = Arc::new(data);

    let mut sk = Skeleton::new(Arc::clone(&data));
    sk.setup_pose();
    sk.update_world_transform(Physics::None);

    // Labels match commands by index, assuming the renderer emits exactly one
    // command per visible region or mesh slot.
    let mut labels: Vec<(String, &'static str, String)> = Vec::new();
    for &slot_id in sk.draw_order.applied() {
        let slot = &sk.slots[slot_id.index()];
        let slot_data = &data.slots[slot_id.index()];
        let _bone_data: &BoneData = &data.bones[slot_data.bone.index()];
        let bone_active = sk.bones[slot_data.bone.index()].active;
        let Some(att_id) = slot.applied().attachment else {
            continue;
        };
        if slot.applied().color.a == 0.0 || !bone_active {
            continue;
        }
        let att = sk.attachment(att_id);
        let (kind, name) = match att {
            Attachment::Region(r) => ("region", r.name.clone()),
            Attachment::Mesh(m) => ("mesh", m.name.clone()),
            Attachment::Clipping(c) => ("clipping", c.name.clone()),
            Attachment::BoundingBox(b) => ("bbox", b.name.clone()),
            Attachment::Path(p) => ("path", p.name.clone()),
            Attachment::Point(p) => ("point", p.name.clone()),
        };
        if matches!(kind, "region" | "mesh") {
            labels.push((slot_data.name.clone(), kind, name));
        }
    }

    let mut renderer = SkeletonRenderer::new();
    let cmds = renderer.render_unbatched(&sk);
    println!("rig={rig} skel={skel}: {} drawable commands", cmds.len());
    for (i, c) in cmds.iter().enumerate() {
        let (xmin, xmax, ymin, ymax) = c.position_bounds().unwrap_or((0.0, 0.0, 0.0, 0.0));
        let (slot_name, kind, att_name) = labels
            .get(i)
            .cloned()
            .unwrap_or_else(|| ("?".to_string(), "?", "?".to_string()));
        let degenerate = (xmax - xmin).abs() < 1e-4 && (ymax - ymin).abs() < 1e-4;
        println!(
            "  cmd[{i:2}] {flag} verts={:3} tris={:3} x=[{xmin:7.1}..{xmax:7.1}] y=[{ymin:7.1}..{ymax:7.1}] slot={slot_name} ({kind}:{att_name})",
            c.num_vertices(),
            c.indices.len() / 3,
            flag = if degenerate { "!!" } else { "  " },
        );
    }
}