import { AnimationState, Physics, Skeleton, SkeletonClipping, Skin } from "@esotericsoftware/spine-core"; interface Rectangle { x: number; y: number; width: number; height: number; } interface GameObject { skeleton?: Skeleton, state?: AnimationState, } export interface SpineBoundsProvider { /** Returns the bounding box for the skeleton, in skeleton space. */ calculateBounds (gameObject: GameObject): Rectangle; } export class AABBRectangleBoundsProvider implements SpineBoundsProvider { constructor ( private x: number, private y: number, private width: number, private height: number, ) { } calculateBounds () { return { x: this.x, y: this.y, width: this.width, height: this.height }; } } export class SetupPoseBoundsProvider implements SpineBoundsProvider { /** * @param clipping If true, clipping attachments are used to compute the bounds. False, by default. */ constructor (private clipping = false) { } calculateBounds (gameObject: GameObject) { if (!gameObject.skeleton) return { x: 0, y: 0, width: 0, height: 0 }; // Make a copy of animation state and skeleton as this might be called while // the skeleton in the GameObject has already been heavily modified. We can not // reconstruct that state. const skeleton = new Skeleton(gameObject.skeleton.data); skeleton.setupPose(); skeleton.updateWorldTransform(Physics.update); const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined); return bounds.width === Number.NEGATIVE_INFINITY ? { x: 0, y: 0, width: 0, height: 0 } : bounds; } } export class SkinsAndAnimationBoundsProvider implements SpineBoundsProvider { /** * @param animation The animation to use for calculating the bounds. If null, the setup pose is used. * @param skins The skins to use for calculating the bounds. If empty, the default skin is used. * @param timeStep The time step to use for calculating the bounds. A smaller time step means more precision, but slower calculation. * @param clipping If true, clipping attachments are used to compute the bounds. False, by default. */ constructor ( private animation?: string, private skins: string[] = [], private timeStep: number = 0.05, private clipping = false, ) { } calculateBounds (gameObject: GameObject): { x: number; y: number; width: number; height: number; } { if (!gameObject.skeleton || !gameObject.state) return { x: 0, y: 0, width: 0, height: 0 }; // Make a copy of animation state and skeleton as this might be called while // the skeleton in the GameObject has already been heavily modified. We can not // reconstruct that state. const animationState = new AnimationState(gameObject.state.data); const skeleton = new Skeleton(gameObject.skeleton.data); const clipper = this.clipping ? new SkeletonClipping() : undefined; const data = skeleton.data; if (this.skins.length > 0) { const customSkin = new Skin("custom-skin"); for (const skinName of this.skins) { const skin = data.findSkin(skinName); if (skin == null) continue; customSkin.addSkin(skin); } skeleton.setSkin(customSkin); } skeleton.setupPose(); const animation = this.animation != null ? data.findAnimation(this.animation!) : null; if (animation == null) { skeleton.updateWorldTransform(Physics.update); const bounds = skeleton.getBoundsRect(clipper); return bounds.width === Number.NEGATIVE_INFINITY ? { x: 0, y: 0, width: 0, height: 0 } : bounds; } else { let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; animationState.clearTracks(); animationState.setAnimation(0, animation, false); const steps = Math.max(animation.duration / this.timeStep, 1.0); for (let i = 0; i < steps; i++) { const delta = i > 0 ? this.timeStep : 0; animationState.update(delta); animationState.apply(skeleton); skeleton.update(delta); skeleton.updateWorldTransform(Physics.update); const bounds = skeleton.getBoundsRect(clipper); minX = Math.min(minX, bounds.x); minY = Math.min(minY, bounds.y); maxX = Math.max(maxX, bounds.x + bounds.width); maxY = Math.max(maxY, bounds.y + bounds.height); } const bounds = { x: minX, y: minY, width: maxX - minX, height: maxY - minY, }; return bounds.width === Number.NEGATIVE_INFINITY ? { x: 0, y: 0, width: 0, height: 0 } : bounds; } } }