diff --git a/src/rendering/sprite/Sprite.ts b/src/rendering/sprite/Sprite.ts index 8b956fc9..6c3975de 100644 --- a/src/rendering/sprite/Sprite.ts +++ b/src/rendering/sprite/Sprite.ts @@ -291,8 +291,12 @@ export class Sprite extends Drawable { return; // failed load shows Texture.missing; nothing to heal } - // Guard against a texture swap or destroy between scheduling and resolution. - if (this._texture === texture && !this.destroyed) { + // Guard against a texture swap or destroy between scheduling and resolution — + // and against an explicit frame set in the meantime (a Spritesheet slicing + // frames out of a still-loading atlas). The schedule-time reset above left a + // 0×0 frame, so a non-empty frame here means someone chose one deliberately; + // only heal the untouched case. + if (this._texture === texture && !this.destroyed && this._textureFrame.width === 0 && this._textureFrame.height === 0) { this.resetTextureFrame(); } } diff --git a/test/rendering/sprite/sprite.test.ts b/test/rendering/sprite/sprite.test.ts index 3df0a5e0..77f77148 100644 --- a/test/rendering/sprite/sprite.test.ts +++ b/test/rendering/sprite/sprite.test.ts @@ -89,6 +89,36 @@ describe('Sprite', () => { expect(Number.isNaN(sprite.scale.x)).toBe(false); expect(sprite.width).toBe(40); }); + + test('an explicit frame set while the texture is loading survives hydration', async () => { + const { texture, finishLoad } = makeDeferredTexture(); + const sprite = new Sprite(texture); + + sprite.setTextureFrame(new Rectangle(64, 0, 32, 32)); + + finishLoad(128, 128); + await texture.loaded; + await Promise.resolve(); // flush the .then microtask + + expect(sprite.textureFrame.x).toBe(64); + expect(sprite.textureFrame.y).toBe(0); + expect(sprite.textureFrame.width).toBe(32); + expect(sprite.textureFrame.height).toBe(32); + }); + + test('heals to the full texture frame when no explicit frame was set before hydration', async () => { + const { texture, finishLoad } = makeDeferredTexture(); + const sprite = new Sprite(texture); + + finishLoad(128, 128); + await texture.loaded; + await Promise.resolve(); // flush the .then microtask + + expect(sprite.textureFrame.x).toBe(0); + expect(sprite.textureFrame.y).toBe(0); + expect(sprite.textureFrame.width).toBe(128); + expect(sprite.textureFrame.height).toBe(128); + }); }); // Binding a destroyed texture is otherwise silent — warn once (dev) at