From 38b5286e108ec6916b445db6e2d96b2815fd4186 Mon Sep 17 00:00:00 2001 From: Vaclav Vancura Date: Mon, 30 Mar 2026 17:31:21 +0200 Subject: [PATCH] fix(api): harden drawSprite, drawBitmapText, and spritesRefresh in BTAPI Validate paletteOffset via assertPaletteIndex in both drawSprite and drawBitmapText, consistent with all other draw methods. Add isIndexized() guard and spriteSheets registration to drawBitmapText, mirroring the existing drawSprite behavior so font sheets are tracked for palette refreshes and the pipeline receives a correctly-typed r8uint texture. Make spritesRefresh resilient: skip and evict non-indexized or destroyed sheets instead of aborting the entire refresh loop on the first failure. Add comment to OffscreenCanvas fallback in test setup warning that its zero-filled pixel data maps to the transparent sentinel; tests requiring opaque pixels must stub OffscreenCanvas before calling indexize(). Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Vaclav Vancura --- src/__test__/setup.ts | 4 ++++ src/core/BTAPI.test.ts | 3 ++- src/core/BTAPI.ts | 31 +++++++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/__test__/setup.ts b/src/__test__/setup.ts index e98cd28..f11babb 100644 --- a/src/__test__/setup.ts +++ b/src/__test__/setup.ts @@ -34,6 +34,10 @@ if (typeof GPUTextureUsage === 'undefined') { }; } +// The fallback OffscreenCanvas returns zero-filled RGBA bytes from getImageData(). +// Zero-alpha pixels map to the transparent sentinel (index 0) in SpriteSheet.indexize(). +// Tests that need specific opaque pixel colors must stub OffscreenCanvas themselves +// (e.g. via vi.stubGlobal('OffscreenCanvas', ...)) before calling indexize(). if (typeof OffscreenCanvas === 'undefined') { (globalThis as unknown as Record).OffscreenCanvas = class { public readonly width: number; diff --git a/src/core/BTAPI.test.ts b/src/core/BTAPI.test.ts index 7c21638..3285979 100644 --- a/src/core/BTAPI.test.ts +++ b/src/core/BTAPI.test.ts @@ -207,7 +207,8 @@ describe('BTAPI', () => { }); it('drawBitmapText should not throw before init', () => { - const mockFont = {} as unknown as BitmapFont; + const mockSheet = { isIndexized: () => true } as unknown as SpriteSheet; + const mockFont = { getSpriteSheet: () => mockSheet } as unknown as BitmapFont; expect(() => BTAPI.instance.drawBitmapText(mockFont, new Vector2i(0, 0), 'hi')).not.toThrow(); }); diff --git a/src/core/BTAPI.ts b/src/core/BTAPI.ts index 4618a5f..29131e5 100644 --- a/src/core/BTAPI.ts +++ b/src/core/BTAPI.ts @@ -415,6 +415,8 @@ export class BTAPI { * @throws If the sprite sheet has not been indexized. */ public drawSprite(spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset: number = 0): void { + this.assertPaletteIndex(paletteOffset); + if (!spriteSheet.isIndexized()) { throw new Error( '[BT] drawSprite: sprite sheet has not been indexized.' + @@ -434,8 +436,21 @@ export class BTAPI { * @param pos - Text position (top-left corner). * @param text - String to render. * @param paletteOffset - Palette index offset applied to all glyphs (default 0). + * @throws If the font's sprite sheet has not been indexized. */ public drawBitmapText(font: BitmapFont, pos: Vector2i, text: string, paletteOffset: number = 0): void { + this.assertPaletteIndex(paletteOffset); + + const sheet = font.getSpriteSheet(); + + if (!sheet.isIndexized()) { + throw new Error( + '[BT] drawBitmapText: font sprite sheet has not been indexized.' + + ' Call spriteSheet.indexize(palette) after setting a palette.', + ); + } + + this.spriteSheets.add(sheet); this.renderer?.drawBitmapText(font, pos, text, paletteOffset); } @@ -452,11 +467,23 @@ export class BTAPI { throw new Error('[BT] spritesRefresh: no active palette. Call BT.paletteSet() first.'); } + let refreshed = 0; + for (const sheet of this.spriteSheets) { - sheet.reindexize(this.palette); + if (!sheet.isIndexized()) { + this.spriteSheets.delete(sheet); + continue; + } + + try { + sheet.reindexize(this.palette); + refreshed++; + } catch { + this.spriteSheets.delete(sheet); + } } - console.log(`[BT] Refreshed ${this.spriteSheets.size} sprite sheet(s) against current palette`); + console.log(`[BT] Refreshed ${refreshed} sprite sheet(s) against current palette`); } // #endregion -- 2.51.2