[ts] Update main dependencies, add formatting script, add Biome for linting, temporary commit

This commit is contained in:
Mario Zechner 2025-07-15 23:56:33 +02:00
parent b544dd99ed
commit 8eb9ba957b
28 changed files with 1418 additions and 433 deletions

1
.gitignore vendored
View File

@ -251,3 +251,4 @@ spine-libgdx/.factorypath
spine-libgdx/.project spine-libgdx/.project
.clang-format .clang-format
spine-c/codegen/spine-cpp-types.json spine-c/codegen/spine-cpp-types.json
spine-flutter/example/devtools_options.yaml

11
spine-ts/biome.json Normal file
View File

@ -0,0 +1,11 @@
{
"formatter": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,8 @@
"prepublish": "npm run clean && npm run build", "prepublish": "npm run clean && npm run build",
"clean": "npx rimraf spine-core/dist spine-canvas/dist spine-canvaskit/dist spine-webgl/dist spine-phaser-v3/dist spine-phaser-v4/dist spine-player/dist spine-threejs/dist spine-pixi-v7/dist spine-pixi-v8/dist spine-webcomponents/dist", "clean": "npx rimraf spine-core/dist spine-canvas/dist spine-canvaskit/dist spine-webgl/dist spine-phaser-v3/dist spine-phaser-v4/dist spine-player/dist spine-threejs/dist spine-pixi-v7/dist spine-pixi-v8/dist spine-webcomponents/dist",
"build": "npm run clean && npm run build:modules && concurrently 'npm run build:core:iife' 'npm run build:core:esm' 'npm run build:canvas:iife' 'npm run build:canvas:esm' 'npm run build:canvaskit:iife' 'npm run build:canvaskit:esm' 'npm run build:webgl:iife' 'npm run build:webgl:esm' 'npm run build:phaser-v3:iife' 'npm run build:phaser-v4:iife' 'npm run build:phaser-v3:esm' 'npm run build:phaser-v4:esm' 'npm run build:player:iife' 'npm run build:player:esm' 'npm run build:player:css' 'npm run build:threejs:iife' 'npm run build:threejs:esm' 'npm run build:pixi-v7:iife' 'npm run build:pixi-v7:esm' 'npm run build:pixi-v8:iife' 'npm run build:pixi-v8:esm' 'npm run build:webcomponents:iife' 'npm run build:webcomponents:esm'", "build": "npm run clean && npm run build:modules && concurrently 'npm run build:core:iife' 'npm run build:core:esm' 'npm run build:canvas:iife' 'npm run build:canvas:esm' 'npm run build:canvaskit:iife' 'npm run build:canvaskit:esm' 'npm run build:webgl:iife' 'npm run build:webgl:esm' 'npm run build:phaser-v3:iife' 'npm run build:phaser-v4:iife' 'npm run build:phaser-v3:esm' 'npm run build:phaser-v4:esm' 'npm run build:player:iife' 'npm run build:player:esm' 'npm run build:player:css' 'npm run build:threejs:iife' 'npm run build:threejs:esm' 'npm run build:pixi-v7:iife' 'npm run build:pixi-v7:esm' 'npm run build:pixi-v8:iife' 'npm run build:pixi-v8:esm' 'npm run build:webcomponents:iife' 'npm run build:webcomponents:esm'",
"format": "npx tsx scripts/format.ts",
"lint": "npx biome lint .",
"postbuild": "npm run minify", "postbuild": "npm run minify",
"build:modules": "npx tsc -b -clean && npx tsc -b", "build:modules": "npx tsc -b -clean && npx tsc -b",
"build:core:iife": "npx esbuild --bundle spine-core/src/index.ts --tsconfig=spine-core/tsconfig.json --sourcemap --outfile=spine-core/dist/iife/spine-core.js --format=iife --global-name=spine", "build:core:iife": "npx esbuild --bundle spine-core/src/index.ts --tsconfig=spine-core/tsconfig.json --sourcemap --outfile=spine-core/dist/iife/spine-core.js --format=iife --global-name=spine",
@ -82,12 +84,15 @@
"spine-webcomponents" "spine-webcomponents"
], ],
"devDependencies": { "devDependencies": {
"@types/offscreencanvas": "^2019.6.4", "@types/offscreencanvas": "^2019.7.3",
"concurrently": "^7.6.0", "concurrently": "^9.2.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"esbuild": "^0.25.4", "esbuild": "^0.25.6",
"alive-server": "^1.3.0", "alive-server": "^1.3.0",
"rimraf": "^3.0.2", "rimraf": "^6.0.1",
"typescript": "5.6.2" "typescript": "^5.8.3",
"typescript-formatter": "^7.2.2",
"@biomejs/biome": "^2.1.1",
"tsx": "^4.19.2"
} }
} }

View File

@ -0,0 +1,38 @@
import { execSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
function findTypeScriptFiles(dir: string, files: string[] = []): string[] {
if (!fs.existsSync(dir)) return files;
fs.readdirSync(dir).forEach(name => {
const filePath = path.join(dir, name);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// Skip node_modules and dist directories
if (name !== 'node_modules' && name !== 'dist') {
findTypeScriptFiles(filePath, files);
}
} else if (name.endsWith('.ts') && !name.endsWith('.d.ts')) {
files.push(filePath);
}
});
return files;
}
// Find all TypeScript files in spine-* directories
const allFiles: string[] = [];
fs.readdirSync('.').forEach(name => {
if (name.startsWith('spine-') && fs.statSync(name).isDirectory()) {
findTypeScriptFiles(name, allFiles);
}
});
if (allFiles.length > 0) {
console.log(`Formatting ${allFiles.length} TypeScript files...`);
execSync(`npx tsfmt -r ${allFiles.join(' ')}`, { stdio: 'inherit' });
} else {
console.log('No TypeScript files found to format.');
}

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true
},
"include": ["*.ts"]
}

View File

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

View File

@ -2,28 +2,28 @@ import * as Phaser from "phaser"
import * as spine from "@esotericsoftware/spine-phaser-v3" import * as spine from "@esotericsoftware/spine-phaser-v3"
class SpineDemo extends Phaser.Scene { class SpineDemo extends Phaser.Scene {
preload() { preload () {
this.load.spineBinary("spineboy-data", "/assets/spineboy-pro.skel"); this.load.spineBinary("spineboy-data", "/assets/spineboy-pro.skel");
this.load.spineAtlas("spineboy-atlas", "/assets/spineboy-pma.atlas"); this.load.spineAtlas("spineboy-atlas", "/assets/spineboy-pma.atlas");
} }
create() { create () {
const spineboy = this.add.spine(400, 500, 'spineboy-data', "spineboy-atlas"); const spineboy = this.add.spine(400, 500, 'spineboy-data', "spineboy-atlas");
spineboy.scale = 0.5; spineboy.scale = 0.5;
spineboy.animationState.setAnimation(0, "walk", true); spineboy.animationState.setAnimation(0, "walk", true);
} }
} }
const config = { const config = {
width: 800, width: 800,
height: 600, height: 600,
type: Phaser.WEBGL, type: Phaser.WEBGL,
scene: [SpineDemo], scene: [SpineDemo],
plugins: { plugins: {
scene: [ scene: [
{ key: "spine.SpinePlugin", plugin: spine.SpinePlugin, mapping: "spine" } { key: "spine.SpinePlugin", plugin: spine.SpinePlugin, mapping: "spine" }
] ]
} }
}; };
new Phaser.Game(config); new Phaser.Game(config);

View File

@ -37,13 +37,13 @@ export const Alpha = components.Alpha;
export interface Type< export interface Type<
T, T,
P extends any[] = any[] P extends any[] = any[]
> extends Function { > extends Function {
new(...args: P): T; new(...args: P): T;
} }
export type Mixin<GameObjectComponent, GameObjectConstraint extends Phaser.GameObjects.GameObject> = < export type Mixin<GameObjectComponent, GameObjectConstraint extends Phaser.GameObjects.GameObject> = <
GameObjectType extends Type<GameObjectConstraint> GameObjectType extends Type<GameObjectConstraint>
>( >(
BaseGameObject: GameObjectType BaseGameObject: GameObjectType
) => GameObjectType & Type<GameObjectComponent>; ) => GameObjectType & Type<GameObjectComponent>;

View File

@ -2,28 +2,28 @@ import * as Phaser from "phaser"
import * as spine from "@esotericsoftware/spine-phaser-v4" import * as spine from "@esotericsoftware/spine-phaser-v4"
class SpineDemo extends Phaser.Scene { class SpineDemo extends Phaser.Scene {
preload() { preload () {
this.load.spineBinary("spineboy-data", "/assets/spineboy-pro.skel"); this.load.spineBinary("spineboy-data", "/assets/spineboy-pro.skel");
this.load.spineAtlas("spineboy-atlas", "/assets/spineboy-pma.atlas"); this.load.spineAtlas("spineboy-atlas", "/assets/spineboy-pma.atlas");
} }
create() { create () {
const spineboy = this.add.spine(400, 500, 'spineboy-data', "spineboy-atlas"); const spineboy = this.add.spine(400, 500, 'spineboy-data', "spineboy-atlas");
spineboy.scale = 0.5; spineboy.scale = 0.5;
spineboy.animationState.setAnimation(0, "walk", true); spineboy.animationState.setAnimation(0, "walk", true);
} }
} }
const config = { const config = {
width: 800, width: 800,
height: 600, height: 600,
type: Phaser.WEBGL, type: Phaser.WEBGL,
scene: [SpineDemo], scene: [SpineDemo],
plugins: { plugins: {
scene: [ scene: [
{ key: "spine.SpinePlugin", plugin: spine.SpinePlugin, mapping: "spine" } { key: "spine.SpinePlugin", plugin: spine.SpinePlugin, mapping: "spine" }
] ]
} }
}; };
new Phaser.Game(config); new Phaser.Game(config);

View File

@ -37,13 +37,13 @@ export const Alpha = components.Alpha;
export interface Type< export interface Type<
T, T,
P extends any[] = any[] P extends any[] = any[]
> extends Function { > extends Function {
new(...args: P): T; new(...args: P): T;
} }
export type Mixin<GameObjectComponent, GameObjectConstraint extends Phaser.GameObjects.GameObject> = < export type Mixin<GameObjectComponent, GameObjectConstraint extends Phaser.GameObjects.GameObject> = <
GameObjectType extends Type<GameObjectConstraint> GameObjectType extends Type<GameObjectConstraint>
>( >(
BaseGameObject: GameObjectType BaseGameObject: GameObjectType
) => GameObjectType & Type<GameObjectComponent>; ) => GameObjectType & Type<GameObjectComponent>;

View File

@ -3,43 +3,43 @@ import { Spine } from '@esotericsoftware/spine-pixi-v7';
/** The PixiJS app Application instance, shared across the project */ /** The PixiJS app Application instance, shared across the project */
export const app = new Application<HTMLCanvasElement>({ export const app = new Application<HTMLCanvasElement>({
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
resolution: window.devicePixelRatio || 1, resolution: window.devicePixelRatio || 1,
autoDensity: true, autoDensity: true,
resizeTo: window, resizeTo: window,
backgroundColor: 0x2c3e50, backgroundColor: 0x2c3e50,
hello: true, hello: true,
}); });
/** Setup app and initialise assets */ /** Setup app and initialise assets */
async function init() { async function init () {
// Add pixi canvas element (app.view) to the document's body // Add pixi canvas element (app.view) to the document's body
document.body.appendChild(app.view); document.body.appendChild(app.view);
// Pre-load the skeleton data and atlas. You can also load .json skeleton data. // Pre-load the skeleton data and atlas. You can also load .json skeleton data.
Assets.add({ alias: "spineboyData", src: "assets/spineboy-pro.skel" }); Assets.add({ alias: "spineboyData", src: "assets/spineboy-pro.skel" });
Assets.add({ alias: "spineboyAtlas", src: "assets/spineboy-pma.atlas" }); Assets.add({ alias: "spineboyAtlas", src: "assets/spineboy-pma.atlas" });
await Assets.load(["spineboyData", "spineboyAtlas"]); await Assets.load(["spineboyData", "spineboyAtlas"]);
// Create the spine display object // Create the spine display object
const spineboy = Spine.from("spineboyData", "spineboyAtlas", { const spineboy = Spine.from("spineboyData", "spineboyAtlas", {
scale: 0.5, scale: 0.5,
}); });
// Set the default mix time to use when transitioning // Set the default mix time to use when transitioning
// from one animation to the next. // from one animation to the next.
spineboy.state.data.defaultMix = 0.2; spineboy.state.data.defaultMix = 0.2;
// Center the spine object on screen. // Center the spine object on screen.
spineboy.x = window.innerWidth / 2; spineboy.x = window.innerWidth / 2;
spineboy.y = window.innerHeight / 2 + spineboy.getBounds().height / 2; spineboy.y = window.innerHeight / 2 + spineboy.getBounds().height / 2;
// Set animation "cape-follow-example" on track 0, looped. // Set animation "cape-follow-example" on track 0, looped.
spineboy.state.setAnimation(0, "run", true); spineboy.state.setAnimation(0, "run", true);
// Add the display object to the stage. // Add the display object to the stage.
app.stage.addChild(spineboy); app.stage.addChild(spineboy);
} }
// Init everything // Init everything

View File

@ -62,11 +62,11 @@ const spineTextureAtlasLoader: AssetExtension<RawAtlas | TextureAtlas, ISpineAtl
name: loaderName, name: loaderName,
}, },
test(url: string): boolean { test (url: string): boolean {
return checkExtension(url, ".atlas"); return checkExtension(url, ".atlas");
}, },
async load(url: string): Promise<RawAtlas> { async load (url: string): Promise<RawAtlas> {
const response = await settings.ADAPTER.fetch(url); const response = await settings.ADAPTER.fetch(url);
const txt = await response.text(); const txt = await response.text();
@ -74,7 +74,7 @@ const spineTextureAtlasLoader: AssetExtension<RawAtlas | TextureAtlas, ISpineAtl
return txt; return txt;
}, },
testParse(asset: unknown, options: ResolvedAsset): Promise<boolean> { testParse (asset: unknown, options: ResolvedAsset): Promise<boolean> {
const isExtensionRight = checkExtension(options.src!, ".atlas"); const isExtensionRight = checkExtension(options.src!, ".atlas");
const isString = typeof asset === "string"; const isString = typeof asset === "string";
const isExplicitLoadParserSet = options.loadParser === loaderName; const isExplicitLoadParserSet = options.loadParser === loaderName;
@ -82,11 +82,11 @@ const spineTextureAtlasLoader: AssetExtension<RawAtlas | TextureAtlas, ISpineAtl
return Promise.resolve((isExtensionRight || isExplicitLoadParserSet) && isString); return Promise.resolve((isExtensionRight || isExplicitLoadParserSet) && isString);
}, },
unload(atlas: TextureAtlas) { unload (atlas: TextureAtlas) {
atlas.dispose(); atlas.dispose();
}, },
async parse(asset: RawAtlas, options: {src: string, data: ISpineAtlasMetadata}, loader: Loader): Promise<TextureAtlas> { async parse (asset: RawAtlas, options: { src: string, data: ISpineAtlasMetadata }, loader: Loader): Promise<TextureAtlas> {
const metadata: ISpineAtlasMetadata = options.data || {}; const metadata: ISpineAtlasMetadata = options.data || {};
let basePath = utils.path.dirname(options.src); let basePath = utils.path.dirname(options.src);

View File

@ -36,11 +36,11 @@ type SkeletonBinaryAsset = Uint8Array;
const loaderName = "spineSkeletonLoader"; const loaderName = "spineSkeletonLoader";
function isJson(resource: any): resource is SkeletonJsonAsset { function isJson (resource: any): resource is SkeletonJsonAsset {
return resource.hasOwnProperty("bones"); return resource.hasOwnProperty("bones");
} }
function isBuffer(resource: any): resource is SkeletonBinaryAsset { function isBuffer (resource: any): resource is SkeletonBinaryAsset {
return resource instanceof Uint8Array; return resource instanceof Uint8Array;
} }
@ -55,18 +55,18 @@ const spineLoaderExtension: AssetExtension<SkeletonJsonAsset | SkeletonBinaryAss
name: loaderName, name: loaderName,
}, },
test(url) { test (url) {
return checkExtension(url, ".skel"); return checkExtension(url, ".skel");
}, },
async load(url: string): Promise<SkeletonBinaryAsset> { async load (url: string): Promise<SkeletonBinaryAsset> {
const response = await settings.ADAPTER.fetch(url); const response = await settings.ADAPTER.fetch(url);
const buffer = new Uint8Array(await response.arrayBuffer()); const buffer = new Uint8Array(await response.arrayBuffer());
return buffer; return buffer;
}, },
testParse(asset: unknown, options: ResolvedAsset): Promise<boolean> { testParse (asset: unknown, options: ResolvedAsset): Promise<boolean> {
const isJsonSpineModel = checkExtension(options.src!, ".json") && isJson(asset); const isJsonSpineModel = checkExtension(options.src!, ".json") && isJson(asset);
const isBinarySpineModel = checkExtension(options.src!, ".skel") && isBuffer(asset); const isBinarySpineModel = checkExtension(options.src!, ".skel") && isBuffer(asset);
const isExplicitLoadParserSet = options.loadParser === loaderName; const isExplicitLoadParserSet = options.loadParser === loaderName;

View File

@ -44,7 +44,7 @@ export class DarkTintBatchGeometry extends Geometry {
* @param {boolean} [_static=false] - Optimization flag, where `false` * @param {boolean} [_static=false] - Optimization flag, where `false`
* is updated every frame, `true` doesn't change frame-to-frame. * is updated every frame, `true` doesn't change frame-to-frame.
*/ */
constructor(_static = false) { constructor (_static = false) {
super(); super();
this._buffer = new Buffer(undefined, _static, false); this._buffer = new Buffer(undefined, _static, false);

View File

@ -38,7 +38,7 @@ export class DarkTintGeometry extends Geometry {
* @param {boolean} [_static=false] - Optimization flag, where `false` * @param {boolean} [_static=false] - Optimization flag, where `false`
* is updated every frame, `true` doesn't change frame-to-frame. * is updated every frame, `true` doesn't change frame-to-frame.
*/ */
constructor(_static = false) { constructor (_static = false) {
super(); super();
const verticesBuffer = new Buffer(undefined); const verticesBuffer = new Buffer(undefined);

View File

@ -94,7 +94,7 @@ export class DarkTintMaterial extends Shader {
private _tintColor: Color; private _tintColor: Color;
private _darkTintColor: Color; private _darkTintColor: Color;
constructor(texture?: Texture) { constructor (texture?: Texture) {
const uniforms = { const uniforms = {
uSampler: texture ?? Texture.EMPTY, uSampler: texture ?? Texture.EMPTY,
alpha: 1, alpha: 1,
@ -127,10 +127,10 @@ export class DarkTintMaterial extends Shader {
this._colorDirty = true; this._colorDirty = true;
} }
public get texture(): Texture { public get texture (): Texture {
return this.uniforms.uSampler; return this.uniforms.uSampler;
} }
public set texture(value: Texture) { public set texture (value: Texture) {
if (this.uniforms.uSampler !== value) { if (this.uniforms.uSampler !== value) {
if (!this.uniforms.uSampler.baseTexture.alphaMode !== !value.baseTexture.alphaMode) { if (!this.uniforms.uSampler.baseTexture.alphaMode !== !value.baseTexture.alphaMode) {
this._colorDirty = true; this._colorDirty = true;
@ -141,7 +141,7 @@ export class DarkTintMaterial extends Shader {
} }
} }
public set alpha(value: number) { public set alpha (value: number) {
if (value === this._alpha) { if (value === this._alpha) {
return; return;
} }
@ -149,11 +149,11 @@ export class DarkTintMaterial extends Shader {
this._alpha = value; this._alpha = value;
this._colorDirty = true; this._colorDirty = true;
} }
public get alpha(): number { public get alpha (): number {
return this._alpha; return this._alpha;
} }
public set tint(value: ColorSource) { public set tint (value: ColorSource) {
if (value === this.tint) { if (value === this.tint) {
return; return;
} }
@ -162,11 +162,11 @@ export class DarkTintMaterial extends Shader {
this._tintRGB = this._tintColor.toLittleEndianNumber(); this._tintRGB = this._tintColor.toLittleEndianNumber();
this._colorDirty = true; this._colorDirty = true;
} }
public get tint(): ColorSource { public get tint (): ColorSource {
return this._tintColor.value!; return this._tintColor.value!;
} }
public set darkTint(value: ColorSource) { public set darkTint (value: ColorSource) {
if (value === this.darkTint) { if (value === this.darkTint) {
return; return;
} }
@ -175,20 +175,20 @@ export class DarkTintMaterial extends Shader {
this._darkTintRGB = this._darkTintColor.toLittleEndianNumber(); this._darkTintRGB = this._darkTintColor.toLittleEndianNumber();
this._colorDirty = true; this._colorDirty = true;
} }
public get darkTint(): ColorSource { public get darkTint (): ColorSource {
return this._darkTintColor.value!; return this._darkTintColor.value!;
} }
public get tintValue(): number { public get tintValue (): number {
return this._tintColor.toNumber(); return this._tintColor.toNumber();
} }
public get darkTintValue(): number { public get darkTintValue (): number {
return this._darkTintColor.toNumber(); return this._darkTintColor.toNumber();
} }
/** Gets called automatically by the Mesh. Intended to be overridden for custom {@link PIXI.MeshMaterial} objects. */ /** Gets called automatically by the Mesh. Intended to be overridden for custom {@link PIXI.MeshMaterial} objects. */
public update(): void { public update (): void {
if (this._colorDirty) { if (this._colorDirty) {
this._colorDirty = false; this._colorDirty = false;
Color.shared.setValue(this._tintColor).premultiply(this._alpha, true).toArray(this.uniforms.uColor); Color.shared.setValue(this._tintColor).premultiply(this._alpha, true).toArray(this.uniforms.uColor);

View File

@ -51,24 +51,24 @@ export class DarkTintMesh extends Mesh<DarkTintMaterial> {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
public _darkTintRGB: number = 0; public _darkTintRGB: number = 0;
constructor(texture?: Texture) { constructor (texture?: Texture) {
super(new DarkTintGeometry(), new DarkTintMaterial(texture), undefined, undefined); super(new DarkTintGeometry(), new DarkTintMaterial(texture), undefined, undefined);
} }
public get darkTint(): ColorSource | null { public get darkTint (): ColorSource | null {
return "darkTint" in this.shader ? (this.shader as unknown as DarkTintMaterial).darkTint : null; return "darkTint" in this.shader ? (this.shader as unknown as DarkTintMaterial).darkTint : null;
} }
public set darkTint(value: ColorSource | null) { public set darkTint (value: ColorSource | null) {
(this.shader as unknown as DarkTintMaterial).darkTint = value!; (this.shader as unknown as DarkTintMaterial).darkTint = value!;
} }
public get darkTintValue(): number { public get darkTintValue (): number {
return (this.shader as unknown as DarkTintMaterial).darkTintValue; return (this.shader as unknown as DarkTintMaterial).darkTintValue;
} }
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
protected override _renderToBatch(renderer: Renderer): void { protected override _renderToBatch (renderer: Renderer): void {
const geometry = this.geometry; const geometry = this.geometry;
const shader = this.shader; const shader = this.shader;

View File

@ -83,7 +83,7 @@ export class DarkTintRenderer extends BatchRenderer {
type: ExtensionType.RendererPlugin, type: ExtensionType.RendererPlugin,
}; };
constructor(renderer: Renderer) { constructor (renderer: Renderer) {
super(renderer); super(renderer);
this.shaderGenerator = new BatchShaderGenerator(vertex, fragment); this.shaderGenerator = new BatchShaderGenerator(vertex, fragment);
this.geometryClass = DarkTintBatchGeometry; this.geometryClass = DarkTintBatchGeometry;
@ -91,7 +91,7 @@ export class DarkTintRenderer extends BatchRenderer {
this.vertexSize = 7; this.vertexSize = 7;
} }
public override packInterleavedGeometry(element: IDarkTintElement, attributeBuffer: ViewableBuffer, indexBuffer: Uint16Array, aIndex: number, iIndex: number): void { public override packInterleavedGeometry (element: IDarkTintElement, attributeBuffer: ViewableBuffer, indexBuffer: Uint16Array, aIndex: number, iIndex: number): void {
const { uint32View, float32View } = attributeBuffer; const { uint32View, float32View } = attributeBuffer;
const packedVertices = aIndex / this.vertexSize; const packedVertices = aIndex / this.vertexSize;
const uvs = element.uvs; const uvs = element.uvs;

View File

@ -4,8 +4,8 @@ export * from './SpineDebugRenderer.js';
export * from './SpineTexture.js'; export * from './SpineTexture.js';
export * from './SlotMesh.js'; export * from './SlotMesh.js';
export * from './DarkSlotMesh.js'; export * from './DarkSlotMesh.js';
export * from './assets/atlasLoader.js'; export * from './assets/Atlas-Loader.js';
export * from './assets/skeletonLoader.js'; export * from './assets/Skeleton-Loader.js';
export * from './darkTintMesh/DarkTintBatchGeom.js'; export * from './darkTintMesh/DarkTintBatchGeom.js';
export * from './darkTintMesh/DarkTintGeom.js'; export * from './darkTintMesh/DarkTintGeom.js';
export * from './darkTintMesh/DarkTintMaterial.js'; export * from './darkTintMesh/DarkTintMaterial.js';
@ -14,5 +14,5 @@ export * from './darkTintMesh/DarkTintRenderer.js';
export * from "@esotericsoftware/spine-core"; export * from "@esotericsoftware/spine-core";
import './assets/atlasLoader.js'; // Side effects install the loaders into pixi import './assets/Atlas-Loader.js'; // Side effects install the loaders into pixi
import './assets/skeletonLoader.js'; // Side effects install the loaders into pixi import './assets/Skeleton-Loader.js'; // Side effects install the loaders into pixi

View File

@ -5,45 +5,45 @@ import { Spine } from '@esotericsoftware/spine-pixi-v8';
export const app = new Application(); export const app = new Application();
/** Setup app and initialise assets */ /** Setup app and initialise assets */
async function init() { async function init () {
await app.init({ await app.init({
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
resolution: window.devicePixelRatio || 1, resolution: window.devicePixelRatio || 1,
autoDensity: true, autoDensity: true,
resizeTo: window, resizeTo: window,
backgroundColor: 0x2c3e50, backgroundColor: 0x2c3e50,
hello: true, hello: true,
}) })
// Add pixi canvas element (app.view) to the document's body // Add pixi canvas element (app.view) to the document's body
document.body.appendChild(app.canvas); document.body.appendChild(app.canvas);
// Pre-load the skeleton data and atlas. You can also load .json skeleton data. // Pre-load the skeleton data and atlas. You can also load .json skeleton data.
Assets.add({ alias: "spineboyData", src: "assets/spineboy-pro.skel" }); Assets.add({ alias: "spineboyData", src: "assets/spineboy-pro.skel" });
Assets.add({ alias: "spineboyAtlas", src: "assets/spineboy-pma.atlas" }); Assets.add({ alias: "spineboyAtlas", src: "assets/spineboy-pma.atlas" });
await Assets.load(["spineboyData", "spineboyAtlas"]); await Assets.load(["spineboyData", "spineboyAtlas"]);
// Create the spine display object // Create the spine display object
const spineboy = Spine.from({ const spineboy = Spine.from({
atlas: "spineboyAtlas", atlas: "spineboyAtlas",
skeleton: "spineboyData", skeleton: "spineboyData",
scale: 0.5, scale: 0.5,
}); });
// Set the default mix time to use when transitioning // Set the default mix time to use when transitioning
// from one animation to the next. // from one animation to the next.
spineboy.state.data.defaultMix = 0.2; spineboy.state.data.defaultMix = 0.2;
// Center the spine object on screen. // Center the spine object on screen.
spineboy.x = window.innerWidth / 2; spineboy.x = window.innerWidth / 2;
spineboy.y = window.innerHeight / 2 + spineboy.getBounds().height / 2; spineboy.y = window.innerHeight / 2 + spineboy.getBounds().height / 2;
// Set animation "cape-follow-example" on track 0, looped. // Set animation "cape-follow-example" on track 0, looped.
spineboy.state.setAnimation(0, "run", true); spineboy.state.setAnimation(0, "run", true);
// Add the display object to the stage. // Add the display object to the stage.
app.stage.addChild(spineboy); app.stage.addChild(spineboy);
} }
// Init everything // Init everything

View File

@ -28,13 +28,13 @@
*****************************************************************************/ *****************************************************************************/
import './require-shim.js'; // Side effects add require pixi.js to global scope import './require-shim.js'; // Side effects add require pixi.js to global scope
import './assets/atlasLoader.js'; // Side effects install the loaders into pixi import './assets/Atlas-Loader.js'; // Side effects install the loaders into pixi
import './assets/skeletonLoader.js'; // Side effects install the loaders into pixi import './assets/Skeleton-Loader.js'; // Side effects install the loaders into pixi
import './darktint/DarkTintBatcher.js'; // Side effects install the batcher into pixi import './darktint/DarkTintBatcher.js'; // Side effects install the batcher into pixi
import './SpinePipe.js'; import './SpinePipe.js';
export * from './assets/atlasLoader.js'; export * from './assets/Atlas-Loader.js';
export * from './assets/skeletonLoader.js'; export * from './assets/Skeleton-Loader.js';
export * from './require-shim.js'; export * from './require-shim.js';
export * from './Spine.js'; export * from './Spine.js';
export * from './SpineDebugRenderer.js'; export * from './SpineDebugRenderer.js';

View File

@ -16,120 +16,120 @@ let lastFrameTime = Date.now() / 1000;
let baseUrl = "/assets/"; let baseUrl = "/assets/";
let skeletonFile = "raptor-pro.json"; let skeletonFile = "raptor-pro.json";
let atlasFile = skeletonFile let atlasFile = skeletonFile
.replace("-pro", "") .replace("-pro", "")
.replace("-ess", "") .replace("-ess", "")
.replace(".json", ".atlas"); .replace(".json", ".atlas");
let animation = "walk"; let animation = "walk";
function init() { function init () {
// create the THREE.JS camera, scene and renderer (WebGL) // create the THREE.JS camera, scene and renderer (WebGL)
let width = window.innerWidth, let width = window.innerWidth,
height = window.innerHeight; height = window.innerHeight;
camera = new THREE.PerspectiveCamera(75, width / height, 1, 3000); camera = new THREE.PerspectiveCamera(75, width / height, 1, 3000);
camera.position.y = 100; camera.position.y = 100;
camera.position.z = 400; camera.position.z = 400;
scene = new THREE.Scene(); scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer(); renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); renderer.setSize(width, height);
document.body.appendChild(renderer.domElement); document.body.appendChild(renderer.domElement);
canvas = renderer.domElement; canvas = renderer.domElement;
controls = new OrbitControls(camera, renderer.domElement); controls = new OrbitControls(camera, renderer.domElement);
// LIGHTS - Ambient // LIGHTS - Ambient
const ambientLight = new THREE.AmbientLight(0xffffff, 7.0) const ambientLight = new THREE.AmbientLight(0xffffff, 7.0)
scene.add(ambientLight) scene.add(ambientLight)
// LIGHTS - spotLight // LIGHTS - spotLight
const spotLight = new THREE.SpotLight(0xffffff, 5, 1200, Math.PI / 4, 0, 0) const spotLight = new THREE.SpotLight(0xffffff, 5, 1200, Math.PI / 4, 0, 0)
spotLight.position.set(0, 1000, 0) spotLight.position.set(0, 1000, 0)
spotLight.castShadow = true spotLight.castShadow = true
spotLight.shadow.mapSize.set(8192, 8192) spotLight.shadow.mapSize.set(8192, 8192)
spotLight.shadow.bias = -0.00001; spotLight.shadow.bias = -0.00001;
scene.add(spotLight) scene.add(spotLight)
// load the assets required to display the Raptor model // load the assets required to display the Raptor model
assetManager = new spine.AssetManager(baseUrl); assetManager = new spine.AssetManager(baseUrl);
assetManager.loadText(skeletonFile); assetManager.loadText(skeletonFile);
assetManager.loadTextureAtlas(atlasFile); assetManager.loadTextureAtlas(atlasFile);
requestAnimationFrame(load); requestAnimationFrame(load);
} }
function load() { function load () {
if (assetManager.isLoadingComplete()) { if (assetManager.isLoadingComplete()) {
// Add a box to the scene to which we attach the skeleton mesh // Add a box to the scene to which we attach the skeleton mesh
geometry = new THREE.BoxGeometry(200, 200, 200); geometry = new THREE.BoxGeometry(200, 200, 200);
material = new THREE.MeshBasicMaterial({ material = new THREE.MeshBasicMaterial({
color: 0xff0000, color: 0xff0000,
wireframe: true, wireframe: true,
}); });
mesh = new THREE.Mesh(geometry, material); mesh = new THREE.Mesh(geometry, material);
scene.add(mesh); scene.add(mesh);
// Load the texture atlas using name.atlas and name.png from the AssetManager. // Load the texture atlas using name.atlas and name.png from the AssetManager.
// The function passed to TextureAtlas is used to resolve relative paths. // The function passed to TextureAtlas is used to resolve relative paths.
atlas = assetManager.require(atlasFile); atlas = assetManager.require(atlasFile);
// Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments // Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
atlasLoader = new spine.AtlasAttachmentLoader(atlas); atlasLoader = new spine.AtlasAttachmentLoader(atlas);
// Create a SkeletonJson instance for parsing the .json file. // Create a SkeletonJson instance for parsing the .json file.
let skeletonJson = new spine.SkeletonJson(atlasLoader); let skeletonJson = new spine.SkeletonJson(atlasLoader);
// Set the scale to apply during parsing, parse the file, and create a new skeleton. // Set the scale to apply during parsing, parse the file, and create a new skeleton.
skeletonJson.scale = 0.4; skeletonJson.scale = 0.4;
let skeletonData = skeletonJson.readSkeletonData( let skeletonData = skeletonJson.readSkeletonData(
assetManager.require(skeletonFile) assetManager.require(skeletonFile)
); );
// Create a SkeletonMesh from the data and attach it to the scene // Create a SkeletonMesh from the data and attach it to the scene
skeletonMesh = new spine.SkeletonMesh({ skeletonMesh = new spine.SkeletonMesh({
skeletonData, skeletonData,
materialFactory(parameters) { materialFactory (parameters) {
return new THREE.MeshStandardMaterial({ ...parameters, metalness: .5 }); return new THREE.MeshStandardMaterial({ ...parameters, metalness: .5 });
}, },
}); });
skeletonMesh.state.setAnimation(0, animation, true); skeletonMesh.state.setAnimation(0, animation, true);
mesh.add(skeletonMesh); mesh.add(skeletonMesh);
requestAnimationFrame(render); requestAnimationFrame(render);
} else requestAnimationFrame(load); } else requestAnimationFrame(load);
} }
let lastTime = Date.now(); let lastTime = Date.now();
function render() { function render () {
// calculate delta time for animation purposes // calculate delta time for animation purposes
let now = Date.now() / 1000; let now = Date.now() / 1000;
let delta = now - lastFrameTime; let delta = now - lastFrameTime;
lastFrameTime = now; lastFrameTime = now;
// resize canvas to use full page, adjust camera/renderer // resize canvas to use full page, adjust camera/renderer
resize(); resize();
// Update orbital controls // Update orbital controls
controls.update(); controls.update();
// update the animation // update the animation
skeletonMesh.update(delta); skeletonMesh.update(delta);
// render the scene // render the scene
renderer.render(scene, camera); renderer.render(scene, camera);
requestAnimationFrame(render); requestAnimationFrame(render);
} }
function resize() { function resize () {
let w = window.innerWidth; let w = window.innerWidth;
let h = window.innerHeight; let h = window.innerHeight;
if (canvas.width != w || canvas.height != h) { if (canvas.width != w || canvas.height != h) {
canvas.width = w; canvas.width = w;
canvas.height = h; canvas.height = h;
} }
camera.aspect = w / h; camera.aspect = w / h;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
renderer.setSize(w, h); renderer.setSize(w, h);
} }
init(); init();

11
tests/biome.json Normal file
View File

@ -0,0 +1,11 @@
{
"formatter": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

View File

@ -3,11 +3,15 @@
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"compare": "tsx compare-with-reference-impl.ts" "compare": "tsx compare-with-reference-impl.ts",
"format": "npx tsfmt -r ./**/*.ts",
"lint": "npx biome lint ."
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"tsx": "^4.0.0" "tsx": "^4.0.0",
"typescript-formatter": "^7.2.2",
"@biomejs/biome": "^2.1.1"
}, },
"dependencies": { "dependencies": {
"@mariozechner/lsp-cli": "^0.1.3" "@mariozechner/lsp-cli": "^0.1.3"

24
tests/tsfmt.json Normal file
View File

@ -0,0 +1,24 @@
{
"baseIndentSize": 0,
"indentSize": 4,
"tabSize": 4,
"indentStyle": 2,
"newLineCharacter": "\n",
"convertTabsToSpaces": false,
"insertSpaceAfterCommaDelimiter": true,
"insertSpaceAfterSemicolonInForStatements": true,
"insertSpaceBeforeAndAfterBinaryOperators": true,
"insertSpaceAfterConstructor": true,
"insertSpaceAfterKeywordsInControlFlowStatements": true,
"insertSpaceAfterFunctionKeywordForAnonymousFunctions": true,
"insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false,
"insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
"insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true,
"insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false,
"insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": false,
"insertSpaceAfterTypeAssertion": false,
"insertSpaceBeforeFunctionParenthesis": true,
"insertSpaceBeforeTypeAnnotation": false,
"placeOpenBraceOnNewLineForFunctions": false,
"placeOpenBraceOnNewLineForControlBlocks": false
}