2025-07-11 14:16:24 +02:00

209 lines
7.7 KiB
TypeScript
Executable File

#!/usr/bin/env npx tsx
/******************************************************************************
* 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.
*****************************************************************************/
import { promises as fs } from 'fs';
import * as path from 'path';
import {
AnimationState,
AnimationStateData,
AtlasAttachmentLoader,
Physics,
Skeleton,
SkeletonBinary,
SkeletonData,
SkeletonJson,
TextureAtlas
} from '../src/index.js';
// Printer class for hierarchical output
class Printer {
private indentLevel = 0;
private readonly INDENT = " ";
print(text: string): void {
const indent = this.INDENT.repeat(this.indentLevel);
console.log(indent + text);
}
indent(): void {
this.indentLevel++;
}
unindent(): void {
this.indentLevel--;
}
printSkeletonData(data: SkeletonData): void {
this.print("SkeletonData {");
this.indent();
this.print(`name: "${data.name || ""}"`);
this.print(`version: ${data.version ? `"${data.version}"` : "null"}`);
this.print(`hash: ${data.hash ? `"${data.hash}"` : "null"}`);
this.print(`x: ${this.formatFloat(data.x)}`);
this.print(`y: ${this.formatFloat(data.y)}`);
this.print(`width: ${this.formatFloat(data.width)}`);
this.print(`height: ${this.formatFloat(data.height)}`);
this.print(`referenceScale: ${this.formatFloat(data.referenceScale)}`);
this.print(`fps: ${this.formatFloat(data.fps || 0)}`);
this.print(`imagesPath: ${data.imagesPath ? `"${data.imagesPath}"` : "null"}`);
this.print(`audioPath: ${data.audioPath ? `"${data.audioPath}"` : "null"}`);
// TODO: Add bones, slots, skins, animations, etc. in future expansion
this.unindent();
this.print("}");
}
printSkeleton(skeleton: Skeleton): void {
this.print("Skeleton {");
this.indent();
this.print(`x: ${this.formatFloat(skeleton.x)}`);
this.print(`y: ${this.formatFloat(skeleton.y)}`);
this.print(`scaleX: ${this.formatFloat(skeleton.scaleX)}`);
this.print(`scaleY: ${this.formatFloat(skeleton.scaleY)}`);
this.print(`time: ${this.formatFloat(skeleton.time)}`);
// TODO: Add runtime state (bones, slots, etc.) in future expansion
this.unindent();
this.print("}");
}
private formatFloat(value: number): string {
// Format to 6 decimal places, matching Java/C++ output
return value.toFixed(6).replace(',', '.');
}
}
// Main DebugPrinter class
class DebugPrinter {
static async main(args: string[]): Promise<void> {
if (args.length < 2) {
console.error("Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]");
process.exit(1);
}
const skeletonPath = args[0];
const atlasPath = args[1];
const animationName = args.length >= 3 ? args[2] : null;
try {
// Load atlas
const atlasData = await fs.readFile(atlasPath, 'utf8');
const atlas = new TextureAtlas(atlasData);
// Load skeleton data
const skeletonData = await this.loadSkeletonData(skeletonPath, atlas);
// Print skeleton data
const printer = new Printer();
console.log("=== SKELETON DATA ===");
printer.printSkeletonData(skeletonData);
// Create skeleton and animation state
const skeleton = new Skeleton(skeletonData);
const stateData = new AnimationStateData(skeletonData);
const state = new AnimationState(stateData);
skeleton.setupPose();
// Set animation or setup pose
if (animationName) {
// Find and set animation
const animation = skeletonData.findAnimation(animationName);
if (!animation) {
console.error(`Animation not found: ${animationName}`);
process.exit(1);
}
state.setAnimation(0, animationName, true);
// Update and apply
state.update(0.016);
state.apply(skeleton);
}
skeleton.updateWorldTransform(Physics.update);
// Print skeleton state
console.log("\n=== SKELETON STATE ===");
printer.printSkeleton(skeleton);
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
private static async loadSkeletonData(skeletonPath: string, atlas: TextureAtlas): Promise<SkeletonData> {
const attachmentLoader = new AtlasAttachmentLoader(atlas);
const ext = path.extname(skeletonPath).toLowerCase();
if (ext === '.json') {
const jsonData = await fs.readFile(skeletonPath, 'utf8');
const json = new SkeletonJson(attachmentLoader);
json.scale = 1;
const skeletonData = json.readSkeletonData(jsonData);
// Set name from filename if not already set
if (!skeletonData.name) {
const basename = path.basename(skeletonPath);
const nameWithoutExt = basename.substring(0, basename.lastIndexOf('.')) || basename;
skeletonData.name = nameWithoutExt;
}
return skeletonData;
} else if (ext === '.skel') {
const binaryData = await fs.readFile(skeletonPath);
const binary = new SkeletonBinary(attachmentLoader);
binary.scale = 1;
const skeletonData = binary.readSkeletonData(new Uint8Array(binaryData));
// Set name from filename if not already set
if (!skeletonData.name) {
const basename = path.basename(skeletonPath);
const nameWithoutExt = basename.substring(0, basename.lastIndexOf('.')) || basename;
skeletonData.name = nameWithoutExt;
}
return skeletonData;
} else {
throw new Error(`Unsupported skeleton file format: ${ext}`);
}
}
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
DebugPrinter.main(process.argv.slice(2));
}
export default DebugPrinter;