diff --git a/src/assets/BitmapFont.test.ts b/src/assets/BitmapFont.test.ts index f617ef1..49413f6 100644 --- a/src/assets/BitmapFont.test.ts +++ b/src/assets/BitmapFont.test.ts @@ -460,6 +460,28 @@ describe('BitmapFont', () => { expect(f.baseline).toBe(12); }); + it('should fall back when size, lineHeight, and baseline are invalid', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + mockFontFetchResponse({ + name: 'BadMetricsFont', + size: -5, + lineHeight: 'not-a-number', + baseline: 0, + texture: 'data:image/png;base64,aGVsbG8=', + glyphs: { A: { x: 0, y: 0, w: 8, h: 12, ox: 0, oy: 0, adv: 9 } }, + }), + ), + ); + + const f = await BitmapFont.load('bad-meta.btfont'); + + expect(f.size).toBe(12); + expect(f.lineHeight).toBe(12); + expect(f.baseline).toBe(12); + }); + it('should use size as a fallback for lineHeight and baseline', async () => { vi.stubGlobal( 'fetch', diff --git a/src/assets/BitmapFont.ts b/src/assets/BitmapFont.ts index c502dc4..36fa1bb 100644 --- a/src/assets/BitmapFont.ts +++ b/src/assets/BitmapFont.ts @@ -271,20 +271,32 @@ export class BitmapFont { const atlasHeight = spriteSheet.height; const { glyphs, asciiGlyphs } = BitmapFont.buildGlyphsFromEntries(glyphEntries, atlasWidth, atlasHeight); + const size = BitmapFont.resolvePositiveFontMetric(data.size, 12); + const lineHeight = BitmapFont.resolvePositiveFontMetric(data.lineHeight, size); + const baseline = BitmapFont.resolvePositiveFontMetric(data.baseline, size); - return new BitmapFont( - spriteSheet, - glyphs, - asciiGlyphs, - data.name || 'Unknown', - data.size || 12, - data.lineHeight || data.size || 12, - data.baseline || data.size || 12, - ); + return new BitmapFont(spriteSheet, glyphs, asciiGlyphs, data.name || 'Unknown', size, lineHeight, baseline); } // #region Loading Helpers + /** + * Coerces a `.btfont` metadata field to a positive finite number. + * + * @param value - Raw JSON value for `size`, `lineHeight`, or `baseline`. + * @param fallback - Value used when `value` is missing or invalid. + * @returns Safe positive metric for {@link BitmapFont} construction. + */ + private static resolvePositiveFontMetric(value: unknown, fallback: number): number { + const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN; + + if (Number.isFinite(parsed) && parsed > 0) { + return parsed; + } + + return fallback; + } + /** * Parses and validates a `.btfont` JSON payload after byte-size checks. * diff --git a/src/render/SoftwareRenderer.test.ts b/src/render/SoftwareRenderer.test.ts index 21f09fe..15574fc 100644 --- a/src/render/SoftwareRenderer.test.ts +++ b/src/render/SoftwareRenderer.test.ts @@ -206,6 +206,30 @@ describe('SoftwareRenderer', () => { expect(getPixel(frame as ImageData, 4, 2, 0)).toEqual([0, 0, 0, 0]); }); + it('clips partial sprite source rectangles while preserving destination alignment', async () => { + const canvas = { + width: 0, + height: 0, + style: { width: '', height: '' }, + getContext: canvasGet2d(context), + toBlob: (_cb: (blob: Blob | null) => void) => {}, + } as unknown as HTMLCanvasElement; + const renderer = new SoftwareRenderer(canvas, new Vector2i(4, 4)); + await renderer.init(); + renderer.setPalette(makePalette()); + + const sheet = SpriteSheet.fromIndexedPixels(2, 2, new Uint8Array([1, 2, 3, 0])); + renderer.beginFrame(); + renderer.drawSprite(sheet, new Rect2i(-1, 0, 2, 2), new Vector2i(0, 0), 0); + renderer.endFrame(); + + const frame = logicalContext.lastImageData; + expect(frame).not.toBeNull(); + expect(getPixel(frame as ImageData, 4, 0, 0)).toEqual([0, 0, 0, 0]); + expect(getPixel(frame as ImageData, 4, 1, 0)).toEqual([255, 0, 0, 255]); + expect(getPixel(frame as ImageData, 4, 1, 1)).toEqual([0, 255, 0, 255]); + }); + it('renders bitmap text through sprite-backed glyphs', async () => { const canvas = { width: 0,