import { getBitmapFontGlyph, getBitmapFontKerning, getBitmapFontMetrics, getBitmapFontPage, } from '@flighthq/bitmapfont/contract'; import { createTextureAtlas } from '@flighthq/textureatlas/contract'; import type { ImportDiagnostic } from '@flighthq/types/contract'; import type { BitmapFont } from '@flighthq/types/contract'; import { describe, expect, it } from 'vitest'; import { parseBitmapFontFnt } from './bitmapFontFnt'; import { parseBitmapFontJson } from './bitmapFontJson'; import { parseBitmapFontXml } from './bitmapFontXml'; const FNT_JSON = JSON.stringify({ chars: [ { chnl: 15, height: 8, id: 65, page: 0, width: 7, x: 0, xadvance: 9, xoffset: 1, y: 0, yoffset: 5 }, { chnl: 15, height: 8, id: 86, page: 0, width: 6, x: 8, xadvance: 8, xoffset: 0, y: 0, yoffset: 0 }, ], common: { base: 26, lineHeight: 32, pages: 1, scaleH: 64, scaleW: 64 }, info: { face: 'Test', size: 32 }, kernings: [{ amount: -2, first: 65, second: 86 }], pages: ['test_0.png'], }); const FNT_TEXT = [ 'common lineHeight=32 base=26 scaleW=64 scaleH=64 pages=1', 'page id=0 file="test_0.png"', 'chars count=2', 'char id=65 x=0 y=0 width=7 height=8 xoffset=1 yoffset=5 xadvance=9 page=0 chnl=15', 'char id=86 x=8 y=0 width=6 height=8 xoffset=0 yoffset=0 xadvance=8 page=0 chnl=15', 'kernings count=1', 'kerning first=65 second=86 amount=-2', ].join('\n'); const FNT_XML = [ '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '', ].join('\n'); describe('parseBitmapFontJson', () => { // A clean parse is two claims: the values are right AND THE PARSER IS NOT COMPLAINING. Every other test // here checks the first. This checks the second — the one that catches a walk that desynchronised and // still left the asserted fields looking plausible. Asserted as an EMPTY list rather than a filter over // truncation-shaped kind names: a pattern built from expected vocabulary silently exempts every kind // whose name nobody guessed, and this importer has kinds like that. it('raises no diagnostic at all for a well-formed file', () => { const diagnostics: ImportDiagnostic[] = []; parseBitmapFontJson(FNT_JSON, { resolvePage: () => createTextureAtlas() }, diagnostics); const complaints = diagnostics.map((diagnostic) => diagnostic.kind); expect(complaints, `a good fnt-json file made the parser complain: ${complaints.join(', ')}`).toEqual([]); }); it('parses glyphs, kerning, and line metrics with the resolved atlas', () => { const atlas = createTextureAtlas(); const font = parseBitmapFontJson(FNT_JSON, { resolvePage: () => atlas }); expect(font).not.toBeNull(); expect(getBitmapFontPage(font!, 0)).toBe(atlas); // yoffset=5 with base=26 → bearingY = base - yoffset = 21 (baseline-relative, up-positive). expect(getBitmapFontGlyph(font!, 65)).toEqual({ advance: 9, bearingX: 1, bearingY: 21, height: 8, page: 0, width: 7, x: 0, y: 0, }); expect(getBitmapFontGlyph(font!, 86)?.advance).toBe(8); expect(getBitmapFontKerning(font!, 65, 86)).toBe(-2); expect(getBitmapFontMetrics(font!)).toEqual({ ascent: 26, descent: 6, lineGap: 0 }); }); it('resolves both pages of a multi-page JSON export and routes each glyph to its page', () => { const page0 = createTextureAtlas(); const page1 = createTextureAtlas(); const multiPage = JSON.stringify({ chars: [ { height: 8, id: 65, page: 0, width: 7, x: 0, xadvance: 9, xoffset: 1, y: 0, yoffset: 0 }, { height: 8, id: 66, page: 1, width: 6, x: 3, xadvance: 8, xoffset: 0, y: 0, yoffset: 0 }, ], common: { base: 26, lineHeight: 32 }, kernings: [], pages: ['test_0.png', 'test_1.png'], }); const font = parseBitmapFontJson(multiPage, { resolvePage: (id) => (id === 0 ? page0 : page1) }); expect(font).not.toBeNull(); expect(font!.pages).toEqual([page0, page1]); expect(getBitmapFontPage(font!, 1)).toBe(page1); expect(getBitmapFontGlyph(font!, 65)!.page).toBe(0); expect(getBitmapFontGlyph(font!, 66)!.page).toBe(1); }); it('reads the sdf/msdf encoding from a distanceField block', () => { const withField = JSON.parse(FNT_JSON) as Record; withField.distanceField = { distanceRange: 4, fieldType: 'msdf' }; const font = parseBitmapFontJson(JSON.stringify(withField), { resolvePage: () => createTextureAtlas() }); expect(font!.encoding).toBe('msdf'); }); it('parses equivalently to the text and XML variants', () => { const atlas = createTextureAtlas(); const resolvePage = () => atlas; const fromJson = parseBitmapFontJson(FNT_JSON, { resolvePage })!; const fromText = parseBitmapFontFnt(FNT_TEXT, { resolvePage })!; const fromXml = parseBitmapFontXml(FNT_XML, { resolvePage })!; for (const codepoint of [65, 86]) { const glyph = getBitmapFontGlyph(fromJson, codepoint); expect(getBitmapFontGlyph(fromText, codepoint)).toEqual(glyph); expect(getBitmapFontGlyph(fromXml, codepoint)).toEqual(glyph); } expectSameKerningAndMetrics(fromText, fromJson); expectSameKerningAndMetrics(fromXml, fromJson); }); it('returns null for malformed JSON without throwing', () => { expect(parseBitmapFontJson('{ not json', { resolvePage: () => createTextureAtlas() })).toBeNull(); expect(parseBitmapFontJson('{}', { resolvePage: () => createTextureAtlas() })).toBeNull(); }); it('returns null when no resolvePage is supplied', () => { expect(parseBitmapFontJson(FNT_JSON)).toBeNull(); }); }); function expectSameKerningAndMetrics(actual: BitmapFont, expected: BitmapFont): void { expect(getBitmapFontKerning(actual, 65, 86)).toBe(getBitmapFontKerning(expected, 65, 86)); expect(getBitmapFontMetrics(actual)).toEqual(getBitmapFontMetrics(expected)); }