Ver Fonte

[c][cpp][libgdx] Launch configs for debug-printer

Mario Zechner há 2 meses atrás
pai
commit
4e3d2be023

+ 22 - 0
spine-c/.vscode/launch.json

@@ -0,0 +1,22 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "debug-printer (c)",
+            "type": "cppdbg",
+            "request": "launch",
+            "program": "${workspaceFolder}/build/debug-printer",
+            "args": [
+                "${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",
+                "${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas",
+                "run"
+            ],
+            "stopAtEntry": false,
+            "cwd": "${workspaceFolder}/build",
+            "environment": [],
+            "externalConsole": false,
+            "MIMode": "lldb",
+            "preLaunchTask": "CMake: build"
+        }
+    ]
+}

+ 9 - 11
spine-c/build.sh

@@ -3,14 +3,12 @@ set -e
 
 cd "$(dirname "$0")"
 
-for arg in "${@:-clean build}"; do
-    case $arg in
-        clean) rm -rf build ;;
-        build) 
-            mkdir -p build && cd build
-            [ -f CMakeCache.txt ] || cmake .. -DCMAKE_BUILD_TYPE=Debug
-            make -j$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)
-            cd ..
-            ;;
-    esac
-done
+# Clean only if explicitly requested
+if [ "$1" = "clean" ]; then
+    rm -rf build
+fi
+
+# Always build
+mkdir -p build && cd build
+[ -f CMakeCache.txt ] || cmake .. -DCMAKE_BUILD_TYPE=Debug
+make -j$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)

+ 24 - 22
spine-c/src/extensions.cpp

@@ -288,21 +288,22 @@ spine_skeleton_data_result spine_skeleton_data_load_json(spine_atlas atlas, cons
     }
 
     // Set name from path if provided
-    if (path != nullptr) {
+    if (path != nullptr && data != nullptr) {
+        String pathStr(path);
+
         // Extract filename without extension from path
-        const char *lastSlash = strrchr(path, '/');
-        const char *lastBackslash = strrchr(path, '\\');
-        const char *start = path;
+        int lastSlash = pathStr.lastIndexOf('/');
+        int lastBackslash = pathStr.lastIndexOf('\\');
+        int start = 0;
 
-        if (lastSlash != nullptr) start = lastSlash + 1;
-        if (lastBackslash != nullptr && lastBackslash > start) start = lastBackslash + 1;
+        if (lastSlash != -1) start = lastSlash + 1;
+        if (lastBackslash != -1 && lastBackslash > start) start = lastBackslash + 1;
 
-        const char *lastDot = strrchr(start, '.');
-        if (lastDot != nullptr) {
-            int length = lastDot - start;
-            data->setName(String(start, length));
+        int lastDot = pathStr.lastIndexOf('.');
+        if (lastDot != -1 && lastDot > start) {
+            data->setName(pathStr.substring(start, lastDot - start));
         } else {
-            data->setName(String(start));
+            data->setName(pathStr.substring(start));
         }
     }
 
@@ -323,21 +324,22 @@ spine_skeleton_data_result spine_skeleton_data_load_binary(spine_atlas atlas, co
     }
 
     // Set name from path if provided
-    if (path != nullptr) {
+    if (path != nullptr && data != nullptr) {
+        String pathStr(path);
+
         // Extract filename without extension from path
-        const char *lastSlash = strrchr(path, '/');
-        const char *lastBackslash = strrchr(path, '\\');
-        const char *start = path;
+        int lastSlash = pathStr.lastIndexOf('/');
+        int lastBackslash = pathStr.lastIndexOf('\\');
+        int start = 0;
 
-        if (lastSlash != nullptr) start = lastSlash + 1;
-        if (lastBackslash != nullptr && lastBackslash > start) start = lastBackslash + 1;
+        if (lastSlash != -1) start = lastSlash + 1;
+        if (lastBackslash != -1 && lastBackslash > start) start = lastBackslash + 1;
 
-        const char *lastDot = strrchr(start, '.');
-        if (lastDot != nullptr) {
-            int length = lastDot - start;
-            data->setName(String(start, length));
+        int lastDot = pathStr.lastIndexOf('.');
+        if (lastDot != -1 && lastDot > start) {
+            data->setName(pathStr.substring(start, lastDot - start));
         } else {
-            data->setName(String(start));
+            data->setName(pathStr.substring(start));
         }
     }
 

+ 24 - 19
spine-c/tests/debug-printer.c

@@ -128,14 +128,16 @@ uint8_t *read_file(const char *path, int *length) {
 }
 
 int main(int argc, char *argv[]) {
-	if (argc < 4) {
-		fprintf(stderr, "Usage: DebugPrinter <skeleton-path> <atlas-path> <animation-name>\n");
+	if (argc < 3) {
+		fprintf(stderr, "Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]\n");
 		return 1;
 	}
 
+	spine_bone_set_y_down(false);
+
 	const char *skeletonPath = argv[1];
 	const char *atlasPath = argv[2];
-	const char *animationName = argv[3];
+	const char *animationName = argc >= 4 ? argv[3] : NULL;
 
 	// Read atlas file
 	int atlasLength = 0;
@@ -210,24 +212,27 @@ int main(int argc, char *argv[]) {
 	spine_animation_state_data stateData = spine_animation_state_data_create(skeletonData);
 	spine_animation_state state = spine_animation_state_create(stateData);
 
-	// Find and set animation
-	spine_animation animation = spine_skeleton_data_find_animation(skeletonData, animationName);
-	if (!animation) {
-		fprintf(stderr, "Animation not found: %s\n", animationName);
-		spine_animation_state_dispose(state);
-		spine_animation_state_data_dispose(stateData);
-		spine_skeleton_dispose(skeleton);
-		spine_skeleton_data_result_dispose(result);
-		spine_atlas_dispose(atlas);
-		return 1;
+	spine_skeleton_setup_pose(skeleton);
+
+	// Set animation or setup pose
+	if (animationName != NULL) {
+		// Find and set animation
+		spine_animation animation = spine_skeleton_data_find_animation(skeletonData, animationName);
+		if (!animation) {
+			fprintf(stderr, "Animation not found: %s\n", animationName);
+			spine_animation_state_dispose(state);
+			spine_animation_state_data_dispose(stateData);
+			spine_skeleton_dispose(skeleton);
+			spine_skeleton_data_result_dispose(result);
+			spine_atlas_dispose(atlas);
+			return 1;
+		}
+		spine_animation_state_set_animation_1(state, 0, animationName, 1);
+		// Update and apply
+		spine_animation_state_update(state, 0.016f);
+		spine_animation_state_apply(state, skeleton);
 	}
 
-	spine_animation_state_set_animation_1(state, 0, animationName, 1);
-
-	// Update and apply
-	spine_animation_state_update(state, 0.016f);
-	spine_animation_state_apply(state, skeleton);
-	spine_skeleton_update(skeleton, 0.016f);
 	spine_skeleton_update_world_transform_1(skeleton, SPINE_PHYSICS_UPDATE);
 
 	// Print skeleton state

+ 22 - 0
spine-cpp/.vscode/launch.json

@@ -0,0 +1,22 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "debug-printer (cpp)",
+            "type": "cppdbg",
+            "request": "launch",
+            "program": "${workspaceFolder}/build/debug-printer",
+            "args": [
+                "${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",
+                "${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas",
+                "run"
+            ],
+            "stopAtEntry": false,
+            "cwd": "${workspaceFolder}/build",
+            "environment": [],
+            "externalConsole": false,
+            "MIMode": "lldb",
+            "preLaunchTask": "CMake: build"
+        }
+    ]
+}

+ 2 - 1
spine-cpp/CMakeLists.txt

@@ -22,5 +22,6 @@ export(
 
 # Optional tests
 if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
-	add_subdirectory(tests)
+    add_executable(debug-printer ${CMAKE_CURRENT_SOURCE_DIR}/tests/DebugPrinter.cpp)
+    target_link_libraries(debug-printer spine-cpp)
 endif()

+ 9 - 11
spine-cpp/build.sh

@@ -3,14 +3,12 @@ set -e
 
 cd "$(dirname "$0")"
 
-for arg in "${@:-clean build}"; do
-    case $arg in
-        clean) rm -rf build ;;
-        build) 
-            mkdir -p build && cd build
-            [ -f CMakeCache.txt ] || cmake .. -DCMAKE_BUILD_TYPE=Debug
-            make -j$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)
-            cd ..
-            ;;
-    esac
-done
+# Clean only if explicitly requested
+if [ "$1" = "clean" ]; then
+    rm -rf build
+fi
+
+# Always build
+mkdir -p build && cd build
+[ -f CMakeCache.txt ] || cmake .. -DCMAKE_BUILD_TYPE=Debug
+make -j$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)

+ 0 - 4
spine-cpp/tests/CMakeLists.txt

@@ -1,4 +0,0 @@
-# Create DebugPrinter executable
-add_executable(DebugPrinter ${CMAKE_CURRENT_SOURCE_DIR}/src/DebugPrinter.cpp)
-target_link_libraries(DebugPrinter spine-cpp)
-target_compile_features(DebugPrinter PRIVATE cxx_std_11)

+ 21 - 16
spine-cpp/tests/src/DebugPrinter.cpp → spine-cpp/tests/DebugPrinter.cpp

@@ -129,14 +129,16 @@ public:
 };
 
 int main(int argc, char *argv[]) {
-	if (argc < 4) {
-		fprintf(stderr, "Usage: DebugPrinter <skeleton-path> <atlas-path> <animation-name>\n");
+	if (argc < 3) {
+		fprintf(stderr, "Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]\n");
 		return 1;
 	}
 
+	Bone::setYDown(false);
+
 	const char *skeletonPath = argv[1];
 	const char *atlasPath = argv[2];
-	const char *animationName = argv[3];
+	const char *animationName = argc >= 4 ? argv[3] : nullptr;
 
 	// Load atlas with headless texture loader
 	HeadlessTextureLoader textureLoader;
@@ -171,21 +173,24 @@ int main(int argc, char *argv[]) {
 	AnimationStateData stateData(skeletonData);
 	AnimationState state(&stateData);
 
-	// Find and set animation
-	Animation *animation = skeletonData->findAnimation(animationName);
-	if (!animation) {
-		fprintf(stderr, "Animation not found: %s\n", animationName);
-		delete skeletonData;
-		delete atlas;
-		return 1;
+	skeleton.setupPose();
+
+	// Set animation or setup pose
+	if (animationName != nullptr) {
+		// Find and set animation
+		Animation *animation = skeletonData->findAnimation(animationName);
+		if (!animation) {
+			fprintf(stderr, "Animation not found: %s\n", animationName);
+			delete skeletonData;
+			delete atlas;
+			return 1;
+		}
+		state.setAnimation(0, animation, true);
+		// Update and apply
+		state.update(0.016f);
+		state.apply(skeleton);
 	}
 
-	state.setAnimation(0, animation, true);
-
-	// Update and apply
-	state.update(0.016f);
-	state.apply(skeleton);
-	skeleton.update(0.016f);
 	skeleton.updateWorldTransform(Physics_Update);
 
 	// Print skeleton state

+ 17 - 0
spine-libgdx/.vscode/launch.json

@@ -0,0 +1,17 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "java",
+            "name": "debug-printer (java)",
+            "request": "launch",
+            "mainClass": "com.esotericsoftware.spine.DebugPrinter",
+            "projectName": "spine-libgdx-tests",
+            "args": [
+                "${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",
+                "${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas",
+                "run"
+            ],
+        }
+    ]
+}

+ 16 - 0
spine-libgdx/spine-libgdx-tests/.vscode/launch.json

@@ -0,0 +1,16 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "java",
+            "name": "DebugPrinter",
+            "request": "launch",
+            "mainClass": "com.esotericsoftware.spine.DebugPrinter",
+            "args": [
+                "../../spine-godot/example-v4-extension/assets/spineboy/spineboy-pro.spine-json",
+                "../../spine-godot/example-v4-extension/assets/spineboy/spineboy.atlas",
+                "idle"
+            ]
+        }
+    ]
+}

+ 18 - 13
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java

@@ -222,18 +222,22 @@ public class DebugPrinter implements ApplicationListener {
 			AnimationStateData stateData = new AnimationStateData(skeletonData);
 			AnimationState state = new AnimationState(stateData);
 
-			// Find and set animation
-			Animation animation = skeletonData.findAnimation(animationName);
-			if (animation == null) {
-				System.err.println("Animation not found: " + animationName);
-				System.exit(1);
+			skeleton.setupPose();
+
+			// Set animation or setup pose
+			if (animationName != null) {
+				// Find and set animation
+				Animation animation = skeletonData.findAnimation(animationName);
+				if (animation == null) {
+					System.err.println("Animation not found: " + animationName);
+					System.exit(1);
+				}
+				state.setAnimation(0, animation, true);
+				// Update and apply
+				state.update(0.016f);
+				state.apply(skeleton);
 			}
 
-			state.setAnimation(0, animation, true);
-
-			// Update and apply
-			state.update(0.016f);
-			state.apply(skeleton);
 			skeleton.update(0.016f);
 			skeleton.updateWorldTransform(Physics.update);
 
@@ -271,13 +275,14 @@ public class DebugPrinter implements ApplicationListener {
 	}
 
 	public static void main (String[] args) {
-		if (args.length < 3) {
-			System.err.println("Usage: DebugPrinter <skeleton-path> <atlas-path> <animation-name>");
+		if (args.length < 2) {
+			System.err.println("Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]");
 			System.exit(1);
 		}
 
 		HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration();
 		config.updatesPerSecond = 60;
-		new HeadlessApplication(new DebugPrinter(args[0], args[1], args[2]), config);
+		String animationName = args.length >= 3 ? args[2] : null;
+		new HeadlessApplication(new DebugPrinter(args[0], args[1], animationName), config);
 	}
 }

+ 1 - 1
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SliderData.java

@@ -33,7 +33,7 @@ import com.badlogic.gdx.utils.Null;
 
 import com.esotericsoftware.spine.TransformConstraintData.FromProperty;
 
-/** Stores the setup pose for a {@link Slider}.  */
+/** Stores the setup pose for a {@link Slider}. */
 public class SliderData extends ConstraintData<Slider, SliderPose> {
 	Animation animation;
 	boolean additive, loop;

+ 210 - 0
spine-ts/spine-core/tests/DebugPrinter.ts

@@ -0,0 +1,210 @@
+#!/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 atlasDir = path.dirname(atlasPath);
+            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;

+ 2 - 1
spine-ts/spine-core/tsconfig.json

@@ -9,7 +9,8 @@
 		"**/*.ts"
 	],
 	"exclude": [
-		"dist/**/*.d.ts"
+		"dist/**/*.d.ts",
+		"tests/**/*.ts"
 	],
 	"references": []
 }

+ 232 - 0
tests/compare-with-reference-impl.ts

@@ -0,0 +1,232 @@
+#!/usr/bin/env npx tsx
+
+import { execSync } from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import { promisify } from 'util';
+
+const writeFile = promisify(fs.writeFile);
+const mkdir = promisify(fs.mkdir);
+
+// Parse command line arguments
+const args = process.argv.slice(2);
+if (args.length < 2) {
+    console.error('Usage: compare-with-reference-impl.ts <skeleton-path> <atlas-path> [animation-name]');
+    process.exit(1);
+}
+
+const [skeletonPath, atlasPath, animationName] = args;
+
+// Get absolute paths
+const absoluteSkeletonPath = path.resolve(skeletonPath);
+const absoluteAtlasPath = path.resolve(atlasPath);
+
+// Script paths
+const scriptDir = path.dirname(new URL(import.meta.url).pathname);
+const rootDir = path.dirname(scriptDir);
+const outputDir = path.join(scriptDir, 'output');
+
+interface RuntimeConfig {
+    name: string;
+    buildCheck: () => boolean;
+    build: () => void;
+    run: () => string;
+}
+
+// Runtime configurations
+const runtimes: RuntimeConfig[] = [
+    {
+        name: 'java',
+        buildCheck: () => {
+            const classPath = path.join(rootDir, 'spine-libgdx/spine-libgdx-tests/build/classes/java/main/com/esotericsoftware/spine/DebugPrinter.class');
+            return fs.existsSync(classPath);
+        },
+        build: () => {
+            console.log('  Building Java runtime...');
+            execSync('./gradlew :spine-libgdx-tests:build', {
+                cwd: path.join(rootDir, 'spine-libgdx'),
+                stdio: 'inherit'
+            });
+        },
+        run: () => {
+            const args = animationName 
+                ? `${absoluteSkeletonPath} ${absoluteAtlasPath} ${animationName}`
+                : `${absoluteSkeletonPath} ${absoluteAtlasPath}`;
+            const output = execSync(
+                `./gradlew -q :spine-libgdx-tests:runDebugPrinter -Pargs="${args}"`,
+                {
+                    cwd: path.join(rootDir, 'spine-libgdx'),
+                    encoding: 'utf8'
+                }
+            );
+            // Find the start of actual output and return everything from there
+            const lines = output.split('\n');
+            const startIndex = lines.findIndex(line => line === '=== SKELETON DATA ===');
+            if (startIndex !== -1) {
+                return lines.slice(startIndex).join('\n').trim();
+            }
+            // Fallback to full output if marker not found
+            return output.trim();
+        }
+    },
+    {
+        name: 'cpp',
+        buildCheck: () => {
+            const execPath = path.join(rootDir, 'spine-cpp/build/debug-printer');
+            return fs.existsSync(execPath);
+        },
+        build: () => {
+            console.log('  Building C++ runtime...');
+            execSync('./build.sh clean', {
+                cwd: path.join(rootDir, 'spine-cpp/'),
+                stdio: 'inherit'
+            });
+        },
+        run: () => {
+            return execSync(
+                `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`,
+                {
+                    cwd: path.join(rootDir, 'spine-cpp'),
+                    encoding: 'utf8'
+                }
+            ).trim();
+        }
+    },
+    {
+        name: 'c',
+        buildCheck: () => {
+            const execPath = path.join(rootDir, 'spine-c/build/debug-printer');
+            return fs.existsSync(execPath);
+        },
+        build: () => {
+            console.log('  Building C runtime...');
+            execSync('./build.sh', {
+                cwd: path.join(rootDir, 'spine-c/'),
+                stdio: 'inherit'
+            });
+        },
+        run: () => {
+            return execSync(
+                `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`,
+                {
+                    cwd: path.join(rootDir, 'spine-c'),
+                    encoding: 'utf8'
+                }
+            ).trim();
+        }
+    },
+    {
+        name: 'ts',
+        buildCheck: () => true, // No build needed
+        build: () => {}, // No build needed
+        run: () => {
+            return execSync(
+                `npx tsx tests/DebugPrinter.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`,
+                {
+                    cwd: path.join(rootDir, 'spine-ts/spine-core'),
+                    encoding: 'utf8'
+                }
+            ).trim();
+        }
+    }
+];
+
+async function main() {
+    // Ensure output directory exists
+    await mkdir(outputDir, { recursive: true });
+
+    console.log('Comparing DebugPrinter outputs for:');
+    console.log(`  Skeleton: ${absoluteSkeletonPath}`);
+    console.log(`  Atlas: ${absoluteAtlasPath}`);
+    console.log(`  Animation: ${animationName}`);
+    console.log('');
+
+    // Run all runtimes and collect outputs
+    const outputs: Record<string, string> = {};
+
+    for (const runtime of runtimes) {
+        console.log(`Running ${runtime.name.toUpperCase()} DebugPrinter...`);
+
+        try {
+            // Build if needed
+            if (!runtime.buildCheck()) {
+                runtime.build();
+            }
+
+            // Run and capture output
+            const output = runtime.run();
+            outputs[runtime.name] = output;
+
+            // Save output to file
+            await writeFile(path.join(outputDir, `${runtime.name}.txt`), output);
+
+            console.log('  Done.');
+        } catch (error) {
+            console.error(`  Error: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
+            outputs[runtime.name] = `Error: ${error instanceof Error ? error.message : JSON.stringify(error)}`;
+        }
+    }
+
+    console.log('');
+    console.log('Comparing outputs...');
+    console.log('');
+
+    // Compare outputs
+    const reference = 'java';
+    let allMatch = true;
+
+    for (const runtime of runtimes) {
+        if (runtime.name !== reference) {
+            process.stdout.write(`Comparing ${reference} vs ${runtime.name}: `);
+
+            if (outputs[reference] === outputs[runtime.name]) {
+                console.log('✓ MATCH');
+            } else {
+                console.log('✗ DIFFER');
+                allMatch = false;
+
+                // Show first few differences
+                const refLines = outputs[reference].split('\n');
+                const runtimeLines = outputs[runtime.name].split('\n');
+                const maxLines = Math.max(refLines.length, runtimeLines.length);
+                let diffCount = 0;
+
+                console.log('  First differences:');
+                for (let i = 0; i < maxLines && diffCount < 5; i++) {
+                    if (refLines[i] !== runtimeLines[i]) {
+                        diffCount++;
+                        console.log(`    Line ${i + 1}:`);
+                        if (refLines[i] !== undefined) {
+                            console.log(`      - ${refLines[i]}`);
+                        }
+                        if (runtimeLines[i] !== undefined) {
+                            console.log(`      + ${runtimeLines[i]}`);
+                        }
+                    }
+                }
+
+                if (diffCount === 0 && refLines.length !== runtimeLines.length) {
+                    console.log(`    Different number of lines: ${refLines.length} vs ${runtimeLines.length}`);
+                }
+
+                // Save outputs for manual diff
+                console.log(`  Full outputs saved to: ${outputDir}/`);
+            }
+        }
+    }
+
+    console.log('');
+    if (allMatch) {
+        console.log('✓ All outputs match!');
+        process.exit(0);
+    } else {
+        console.log(`✗ Outputs differ. Check the output files in ${outputDir}/`);
+        process.exit(1);
+    }
+}
+
+// Run main function
+main().catch(error => {
+    console.error('Error:', error);
+    process.exit(1);
+});

+ 573 - 0
tests/package-lock.json

@@ -0,0 +1,573 @@
+{
+  "name": "spine-tests",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "spine-tests",
+      "devDependencies": {
+        "@types/node": "^20.0.0",
+        "tsx": "^4.0.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
+      "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",
+      "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",
+      "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",
+      "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",
+      "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",
+      "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",
+      "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",
+      "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",
+      "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",
+      "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",
+      "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",
+      "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",
+      "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",
+      "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",
+      "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",
+      "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",
+      "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",
+      "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",
+      "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",
+      "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",
+      "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz",
+      "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",
+      "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",
+      "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",
+      "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",
+      "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "20.19.7",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.7.tgz",
+      "integrity": "sha512-1GM9z6BJOv86qkPvzh2i6VW5+VVrXxCLknfmTkWEqz+6DqosiY28XUWCTmBcJ0ACzKqx/iwdIREfo1fwExIlkA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.6",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
+      "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.6",
+        "@esbuild/android-arm": "0.25.6",
+        "@esbuild/android-arm64": "0.25.6",
+        "@esbuild/android-x64": "0.25.6",
+        "@esbuild/darwin-arm64": "0.25.6",
+        "@esbuild/darwin-x64": "0.25.6",
+        "@esbuild/freebsd-arm64": "0.25.6",
+        "@esbuild/freebsd-x64": "0.25.6",
+        "@esbuild/linux-arm": "0.25.6",
+        "@esbuild/linux-arm64": "0.25.6",
+        "@esbuild/linux-ia32": "0.25.6",
+        "@esbuild/linux-loong64": "0.25.6",
+        "@esbuild/linux-mips64el": "0.25.6",
+        "@esbuild/linux-ppc64": "0.25.6",
+        "@esbuild/linux-riscv64": "0.25.6",
+        "@esbuild/linux-s390x": "0.25.6",
+        "@esbuild/linux-x64": "0.25.6",
+        "@esbuild/netbsd-arm64": "0.25.6",
+        "@esbuild/netbsd-x64": "0.25.6",
+        "@esbuild/openbsd-arm64": "0.25.6",
+        "@esbuild/openbsd-x64": "0.25.6",
+        "@esbuild/openharmony-arm64": "0.25.6",
+        "@esbuild/sunos-x64": "0.25.6",
+        "@esbuild/win32-arm64": "0.25.6",
+        "@esbuild/win32-ia32": "0.25.6",
+        "@esbuild/win32-x64": "0.25.6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/get-tsconfig": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
+      "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
+    "node_modules/tsx": {
+      "version": "4.20.3",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
+      "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "~0.25.0",
+        "get-tsconfig": "^4.7.5"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    }
+  }
+}

+ 12 - 0
tests/package.json

@@ -0,0 +1,12 @@
+{
+  "name": "spine-tests",
+  "type": "module",
+  "private": true,
+  "scripts": {
+    "compare": "tsx compare-with-reference-impl.ts"
+  },
+  "devDependencies": {
+    "@types/node": "^20.0.0",
+    "tsx": "^4.0.0"
+  }
+}

+ 17 - 0
tests/tsconfig.json

@@ -0,0 +1,17 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "types": ["node"]
+  },
+  "include": [
+    "*.ts"
+  ]
+}