@flighthq/bitmapfont 0.3.0-next.906.07cea63 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/bitmapFont.d.ts +5 -1
  2. package/dist/bitmapFont.d.ts.map +1 -1
  3. package/dist/bitmapFont.js +55 -7
  4. package/dist/bitmapFont.js.map +1 -1
  5. package/dist/bitmapFontFromGlyphAtlas.d.ts +19 -0
  6. package/dist/bitmapFontFromGlyphAtlas.d.ts.map +1 -0
  7. package/dist/bitmapFontFromGlyphAtlas.js +46 -0
  8. package/dist/bitmapFontFromGlyphAtlas.js.map +1 -0
  9. package/dist/bitmapFontGlyphSource.d.ts.map +1 -1
  10. package/dist/bitmapFontGlyphSource.js +2 -1
  11. package/dist/bitmapFontGlyphSource.js.map +1 -1
  12. package/dist/contract.d.ts +4 -0
  13. package/dist/contract.d.ts.map +1 -1
  14. package/dist/contract.js +4 -0
  15. package/dist/contract.js.map +1 -1
  16. package/dist/enableBitmapFontGuards.d.ts +18 -0
  17. package/dist/enableBitmapFontGuards.d.ts.map +1 -0
  18. package/dist/enableBitmapFontGuards.js +39 -0
  19. package/dist/enableBitmapFontGuards.js.map +1 -0
  20. package/dist/explainBitmapFontGlyph.d.ts +14 -0
  21. package/dist/explainBitmapFontGlyph.d.ts.map +1 -0
  22. package/dist/explainBitmapFontGlyph.js +27 -0
  23. package/dist/explainBitmapFontGlyph.js.map +1 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/summarizeBitmapFont.d.ts +14 -0
  29. package/dist/summarizeBitmapFont.d.ts.map +1 -0
  30. package/dist/summarizeBitmapFont.js +34 -0
  31. package/dist/summarizeBitmapFont.js.map +1 -0
  32. package/package.json +8 -3
  33. package/src/bitmapFont.test.ts +210 -10
  34. package/src/bitmapFontFromGlyphAtlas.test.ts +81 -0
  35. package/src/bitmapFontGlyphSource.test.ts +7 -7
  36. package/src/enableBitmapFontGuards.test.ts +76 -0
  37. package/src/explainBitmapFontGlyph.test.ts +73 -0
  38. package/src/summarizeBitmapFont.test.ts +86 -0
@@ -1,8 +1,12 @@
1
- import type { BitmapFont, BitmapFontData, GlyphEntry, GlyphMetrics, TextureAtlas } from '@flighthq/types/contract';
1
+ import type { BitmapFont, BitmapFontData, BitmapFontKerningPair, GlyphEntry, GlyphMetrics, TextureAtlas } from '@flighthq/types/contract';
2
2
  export declare function createBitmapFont(data: Readonly<BitmapFontData>): BitmapFont;
3
3
  export declare function getBitmapFontGlyph(font: Readonly<BitmapFont>, codepoint: number): GlyphEntry | null;
4
4
  export declare function getBitmapFontKerning(font: Readonly<BitmapFont>, left: number, right: number): number;
5
5
  export declare function getBitmapFontMetrics(font: Readonly<BitmapFont>): Readonly<GlyphMetrics>;
6
6
  export declare function getBitmapFontPage(font: Readonly<BitmapFont>, page?: number): TextureAtlas | null;
7
7
  export declare function getBitmapFontPages(font: Readonly<BitmapFont>): readonly TextureAtlas[];
8
+ export declare function hasBitmapFontGlyph(font: Readonly<BitmapFont>, codepoint: number): boolean;
9
+ export declare function packBitmapFontKerningKey(left: number, right: number): number;
10
+ export declare function setBitmapFontGuard(guard: ((reason: string, codepoint: number, page: number) => void) | null): void;
11
+ export declare function unpackBitmapFontKerningKey(key: number, out: BitmapFontKerningPair): BitmapFontKerningPair;
8
12
  //# sourceMappingURL=bitmapFont.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bitmapFont.d.ts","sourceRoot":"","sources":["../src/bitmapFont.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AASnH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,UAAU,CAiC3E;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEnG;AAID,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpG;AAGD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,CAEvF;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,SAAI,GAAG,YAAY,GAAG,IAAI,CAE3F;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,SAAS,YAAY,EAAE,CAEtF"}
1
+ {"version":3,"file":"bitmapFont.d.ts","sourceRoot":"","sources":["../src/bitmapFont.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,YAAY,EACb,MAAM,0BAA0B,CAAC;AASlC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,UAAU,CAiC3E;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEnG;AAID,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpG;AAGD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,CAEvF;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,SAAI,GAAG,YAAY,GAAG,IAAI,CAE3F;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,SAAS,YAAY,EAAE,CAEtF;AAMD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAEzF;AAeD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5E;AAKD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAElH;AASD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,qBAAqB,GAAG,qBAAqB,CAIzG"}
@@ -1,5 +1,5 @@
1
1
  // Builds an immutable static bitmap font from plain data: the glyph list becomes a
2
- // `codepoint → GlyphEntry` map, the kerning pairs become a `(left << 16) | right → amount` map, and
2
+ // `codepoint → GlyphEntry` map, the kerning pairs become a `left * 0x110000 + right → amount` map, and
3
3
  // the page-indexed atlas list, line metrics, and encoding (default `raster`) are carried as-is. Each
4
4
  // glyph's `page` (default 0) indexes `data.pages`; an out-of-range page is clamped to 0 so the glyph
5
5
  // is still placed (on the primary page) rather than dropped — a bad page index is a source-data
@@ -15,7 +15,7 @@ export function createBitmapFont(data) {
15
15
  bearingX: glyph.bearingX,
16
16
  bearingY: glyph.bearingY,
17
17
  height: glyph.height,
18
- page: page >= 0 && page < pageCount ? page : 0,
18
+ page: resolveBitmapFontGlyphPage(glyph.codepoint, page, pageCount),
19
19
  width: glyph.width,
20
20
  x: glyph.x,
21
21
  y: glyph.y,
@@ -64,10 +64,58 @@ export function getBitmapFontPage(font, page = 0) {
64
64
  export function getBitmapFontPages(font) {
65
65
  return font.pages;
66
66
  }
67
- // Packs an adjacent glyph pair into the single-number kerning-map key `(left << 16) | right`. Both
68
- // codepoints are assumed to lie in the Basic Multilingual Plane (< 0x10000); supplementary-plane
69
- // pairs are outside this table's addressable range.
70
- function packBitmapFontKerningKey(left, right) {
71
- return (left << 16) | right;
67
+ // Whether the font carries a glyph for `codepoint`. Distinct from `getBitmapFontGlyph(...) !== null`
68
+ // only in intent, but that intent is the point: a caller choosing a fallback font, or filtering a string
69
+ // to what this font can draw, asks a question about coverage and should not have to name the sentinel to
70
+ // get an answer. Cheap enough for a per-character loop — one map lookup, no allocation.
71
+ export function hasBitmapFontGlyph(font, codepoint) {
72
+ return font.glyphs.has(codepoint);
72
73
  }
74
+ // Packs an adjacent glyph pair into the single-number kerning-map key `left * 0x110000 + right`. The
75
+ // inverse is `unpackBitmapFontKerningKey`; the two are one primitive and must move together.
76
+ //
77
+ // Multiplication rather than `(left << 16) | right`, because Unicode does not fit in 16 bits and the
78
+ // shift silently ALIASED rather than failing. JavaScript's bitwise operators truncate to 32 bits, so a
79
+ // supplementary-plane left glyph wrapped into another pair's key: U+10000 followed by 'A' produced the
80
+ // same key as U+0000 followed by 'A', and U+1F600 the same as U+F600. A font with emoji kerning would
81
+ // return the wrong adjustment for an unrelated BMP pair, which is worse than returning none.
82
+ //
83
+ // `0x110000` is the Unicode codepoint space (U+0000..U+10FFFF inclusive), so every pair maps to a
84
+ // distinct key. The largest key is 0x10FFFF * 0x110000 + 0x10FFFF, about 1.24e12 — comfortably inside
85
+ // the 2^53 range where a double holds every integer exactly, so this stays exact arithmetic and never
86
+ // touches the 32-bit bitwise path. A C++ port would use a 64-bit integer key for the same reason.
87
+ export function packBitmapFontKerningKey(left, right) {
88
+ return left * UNICODE_CODEPOINT_SPACE + right;
89
+ }
90
+ // Installs the caller-facing guard invoked when `createBitmapFont` silently repairs source data. The
91
+ // core carries the seam and never the message: `@flighthq/bitmapfont` has no dependency on
92
+ // `@flighthq/log`, and the wording lives in the separately-importable `enableBitmapFontGuards`.
93
+ export function setBitmapFontGuard(guard) {
94
+ _guard = guard;
95
+ }
96
+ // Recovers the adjacent glyph pair a kerning key encodes, into `out`. The exact inverse of
97
+ // `packBitmapFontKerningKey`, and the reason a codec walking `font.kerning.keys()` never hand-rolls the
98
+ // arithmetic: a `>>> 16` / `& 0xffff` inverse reads a 0x110000-radix key as a 16-bit one, emitting
99
+ // garbage codepoints for ordinary BMP pairs and truncating every key past 2^32 outright.
100
+ //
101
+ // Division and remainder, not shifts, for the same exactness reason the pack multiplies — the key
102
+ // leaves the 32-bit range well inside the supported codepoint space.
103
+ export function unpackBitmapFontKerningKey(key, out) {
104
+ out.left = Math.floor(key / UNICODE_CODEPOINT_SPACE);
105
+ out.right = key % UNICODE_CODEPOINT_SPACE;
106
+ return out;
107
+ }
108
+ // The page a glyph is placed on. An out-of-range index is clamped to the primary page so the glyph is
109
+ // still drawn — a bad page index is a source-data defect the font should survive rather than a reason to
110
+ // lose a glyph — but the clamp is reported through the guard seam, because a silently relocated glyph
111
+ // renders the wrong pixels and looks like a packing bug rather than a bad font file.
112
+ function resolveBitmapFontGlyphPage(codepoint, page, pageCount) {
113
+ if (page >= 0 && page < pageCount)
114
+ return page;
115
+ _guard?.('page-out-of-range', codepoint, page);
116
+ return 0;
117
+ }
118
+ // U+0000..U+10FFFF inclusive — the stride that keeps one pair's key out of the next pair's range.
119
+ const UNICODE_CODEPOINT_SPACE = 0x110000;
120
+ let _guard = null;
73
121
  //# sourceMappingURL=bitmapFont.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bitmapFont.js","sourceRoot":"","sources":["../src/bitmapFont.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bitmapFont.js","sourceRoot":"","sources":["../src/bitmapFont.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { BitmapFont, GlyphAtlas } from '@flighthq/types/contract';
2
+ /** Freezes a live `GlyphAtlas` into a static `BitmapFont` — the dynamic cache's contents as a fixed
3
+ * font, so a build step can bake what a run actually rasterized and ship it instead of rasterizing
4
+ * again at startup.
5
+ *
6
+ * This lives in `@flighthq/bitmapfont` rather than in `@flighthq/glyphatlas` because a conversion
7
+ * constructor belongs to the package that owns the PRODUCT, the same way `createImageResourceFromBitmap`
8
+ * lives in `@flighthq/image`. Every type is in `@flighthq/types`, so reading a `GlyphAtlas` here costs
9
+ * no dependency on the atlas package, and `glyphatlas` gains no edge to `textureatlas` or `texture`.
10
+ *
11
+ * The page is CPU-only: the atlas's own bitmap is already a valid `TextureSource` under the flat
12
+ * texture model, so the font's page is a `TextureAtlas` over a bitmap-sourced `Texture` with no GPU
13
+ * work and no readback. Whichever backend consumes the font uploads it later through the ordinary
14
+ * kind-keyed resolver path, exactly as it would for any other bitmap-sourced texture.
15
+ *
16
+ * The result is a snapshot: later rasterization into the source atlas does not reach the font, and the
17
+ * font holds its own reference to the atlas bitmap. */
18
+ export declare function createBitmapFontFromGlyphAtlas(atlas: Readonly<GlyphAtlas>): BitmapFont;
19
+ //# sourceMappingURL=bitmapFontFromGlyphAtlas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bitmapFontFromGlyphAtlas.d.ts","sourceRoot":"","sources":["../src/bitmapFontFromGlyphAtlas.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAuB,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAI5F;;;;;;;;;;;;;;;wDAewD;AACxD,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,UAAU,CAyBtF"}
@@ -0,0 +1,46 @@
1
+ import { createTexture } from '@flighthq/texture/contract';
2
+ import { createTextureAtlas } from '@flighthq/textureatlas/contract';
3
+ import { createBitmapFont } from './bitmapFont';
4
+ /** Freezes a live `GlyphAtlas` into a static `BitmapFont` — the dynamic cache's contents as a fixed
5
+ * font, so a build step can bake what a run actually rasterized and ship it instead of rasterizing
6
+ * again at startup.
7
+ *
8
+ * This lives in `@flighthq/bitmapfont` rather than in `@flighthq/glyphatlas` because a conversion
9
+ * constructor belongs to the package that owns the PRODUCT, the same way `createImageResourceFromBitmap`
10
+ * lives in `@flighthq/image`. Every type is in `@flighthq/types`, so reading a `GlyphAtlas` here costs
11
+ * no dependency on the atlas package, and `glyphatlas` gains no edge to `textureatlas` or `texture`.
12
+ *
13
+ * The page is CPU-only: the atlas's own bitmap is already a valid `TextureSource` under the flat
14
+ * texture model, so the font's page is a `TextureAtlas` over a bitmap-sourced `Texture` with no GPU
15
+ * work and no readback. Whichever backend consumes the font uploads it later through the ordinary
16
+ * kind-keyed resolver path, exactly as it would for any other bitmap-sourced texture.
17
+ *
18
+ * The result is a snapshot: later rasterization into the source atlas does not reach the font, and the
19
+ * font holds its own reference to the atlas bitmap. */
20
+ export function createBitmapFontFromGlyphAtlas(atlas) {
21
+ const runtime = atlas.runtime;
22
+ const glyphs = [];
23
+ for (const [codepoint, entry] of runtime.entries) {
24
+ glyphs.push({
25
+ advance: entry.advance,
26
+ bearingX: entry.bearingX,
27
+ bearingY: entry.bearingY,
28
+ codepoint,
29
+ height: entry.height,
30
+ // The dynamic atlas is a single growing bitmap, so every glyph is on page 0.
31
+ page: 0,
32
+ width: entry.width,
33
+ x: entry.x,
34
+ y: entry.y,
35
+ });
36
+ }
37
+ // Regions stay empty: a bitmap font addresses its glyphs through its own glyph table, so the page
38
+ // exists to carry the pixels, not to name sub-rectangles.
39
+ const page = createTextureAtlas({ texture: createTexture({ source: runtime.bitmap }) });
40
+ return createBitmapFont({
41
+ glyphs,
42
+ metrics: { ...runtime.metrics },
43
+ pages: [page],
44
+ });
45
+ }
46
+ //# sourceMappingURL=bitmapFontFromGlyphAtlas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bitmapFontFromGlyphAtlas.js","sourceRoot":"","sources":["../src/bitmapFontFromGlyphAtlas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;;;;;wDAewD;AACxD,MAAM,UAAU,8BAA8B,CAAC,KAA2B;IACxE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9B,MAAM,MAAM,GAA0B,EAAE,CAAC;IACzC,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACjD,MAAM,CAAC,IAAI,CAAC;YACV,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,SAAS;YACT,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,6EAA6E;YAC7E,IAAI,EAAE,CAAC;YACP,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,CAAC,EAAE,KAAK,CAAC,CAAC;YACV,CAAC,EAAE,KAAK,CAAC,CAAC;SACX,CAAC,CAAC;IACL,CAAC;IACD,kGAAkG;IAClG,0DAA0D;IAC1D,MAAM,IAAI,GAAG,kBAAkB,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IACxF,OAAO,gBAAgB,CAAC;QACtB,MAAM;QACN,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE;QAC/B,KAAK,EAAE,CAAC,IAAI,CAAC;KACd,CAAC,CAAC;AACL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"bitmapFontGlyphSource.d.ts","sourceRoot":"","sources":["../src/bitmapFontGlyphSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAQxE,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,WAAW,CAiBvF"}
1
+ {"version":3,"file":"bitmapFontGlyphSource.d.ts","sourceRoot":"","sources":["../src/bitmapFontGlyphSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAQxE,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,WAAW,CAkBvF"}
@@ -8,7 +8,8 @@ export function createGlyphSourceFromBitmapFont(font) {
8
8
  getGlyphAtlasImage(page = 0) {
9
9
  // Each page's atlas image, indexed by the glyph's `page`. An out-of-range page (or a page whose
10
10
  // atlas carries no image yet) yields null — the renderer skips glyphs it cannot sample.
11
- return font.pages[page]?.image ?? null;
11
+ const texture = font.pages[page]?.texture;
12
+ return texture?.dimension === '2d' ? texture.source : null;
12
13
  },
13
14
  getGlyphEntry(codepoint) {
14
15
  return getBitmapFontGlyph(font, codepoint);
@@ -1 +1 @@
1
- {"version":3,"file":"bitmapFontGlyphSource.js","sourceRoot":"","sources":["../src/bitmapFontGlyphSource.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAE9F,sGAAsG;AACtG,mGAAmG;AACnG,0FAA0F;AAC1F,yGAAyG;AACzG,MAAM,UAAU,+BAA+B,CAAC,IAA0B;IACxE,OAAO;QACL,kBAAkB,CAAC,IAAI,GAAG,CAAC;YACzB,gGAAgG;YAChG,wFAAwF;YACxF,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;QACzC,CAAC;QACD,aAAa,CAAC,SAAS;YACrB,OAAO,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAC7C,CAAC;QACD,eAAe,CAAC,IAAI,EAAE,KAAK;YACzB,OAAO,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACjD,CAAC;QACD,eAAe;YACb,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACpC,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"bitmapFontGlyphSource.js","sourceRoot":"","sources":["../src/bitmapFontGlyphSource.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAE9F,sGAAsG;AACtG,mGAAmG;AACnG,0FAA0F;AAC1F,yGAAyG;AACzG,MAAM,UAAU,+BAA+B,CAAC,IAA0B;IACxE,OAAO;QACL,kBAAkB,CAAC,IAAI,GAAG,CAAC;YACzB,gGAAgG;YAChG,wFAAwF;YACxF,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;YAC1C,OAAO,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7D,CAAC;QACD,aAAa,CAAC,SAAS;YACrB,OAAO,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAC7C,CAAC;QACD,eAAe,CAAC,IAAI,EAAE,KAAK;YACzB,OAAO,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACjD,CAAC;QACD,eAAe;YACb,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACpC,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,3 +1,7 @@
1
1
  export * from './bitmapFont';
2
+ export * from './bitmapFontFromGlyphAtlas';
2
3
  export * from './bitmapFontGlyphSource';
4
+ export * from './enableBitmapFontGuards';
5
+ export * from './explainBitmapFontGlyph';
6
+ export * from './summarizeBitmapFont';
3
7
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC"}
package/dist/contract.js CHANGED
@@ -1,3 +1,7 @@
1
1
  export * from './bitmapFont';
2
+ export * from './bitmapFontFromGlyphAtlas';
2
3
  export * from './bitmapFontGlyphSource';
4
+ export * from './enableBitmapFontGuards';
5
+ export * from './explainBitmapFontGlyph';
6
+ export * from './summarizeBitmapFont';
3
7
  //# sourceMappingURL=contract.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /** Uninstalls the guard installed by `enableBitmapFontGuards`. */
2
+ export declare function disableBitmapFontGuards(): void;
3
+ /**
4
+ * Installs the caller-facing bitmap-font guard (opt-in, dev-only). `createBitmapFont` repairs bad source
5
+ * data rather than rejecting it, and a repair is invisible from the outside — the font builds, every
6
+ * lookup succeeds, and the glyph simply samples the wrong page. This warns once, through `@flighthq/log`,
7
+ * for the repair it makes:
8
+ *
9
+ * **A glyph names a page the font does not have.** The glyph is clamped onto the primary page so it is
10
+ * still drawn, which is the right call for a font that must survive a bad asset — but it samples
11
+ * whatever happens to sit at those coordinates on page 0. That renders as a garbled glyph or a slice of
12
+ * a neighbour, which reads as an atlas-packing bug rather than the font-file defect it is.
13
+ *
14
+ * Pair with `explainBitmapFontGlyph` for a pull-style answer about one codepoint. Not importing this
15
+ * module costs production nothing: the messages and the `@flighthq/log` dependency live only here.
16
+ */
17
+ export declare function enableBitmapFontGuards(): void;
18
+ //# sourceMappingURL=enableBitmapFontGuards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableBitmapFontGuards.d.ts","sourceRoot":"","sources":["../src/enableBitmapFontGuards.ts"],"names":[],"mappings":"AAKA,kEAAkE;AAClE,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
@@ -0,0 +1,39 @@
1
+ import { logOnce } from '@flighthq/log/contract';
2
+ import { LogLevel } from '@flighthq/types/contract';
3
+ import { setBitmapFontGuard } from './bitmapFont';
4
+ /** Uninstalls the guard installed by `enableBitmapFontGuards`. */
5
+ export function disableBitmapFontGuards() {
6
+ setBitmapFontGuard(null);
7
+ }
8
+ /**
9
+ * Installs the caller-facing bitmap-font guard (opt-in, dev-only). `createBitmapFont` repairs bad source
10
+ * data rather than rejecting it, and a repair is invisible from the outside — the font builds, every
11
+ * lookup succeeds, and the glyph simply samples the wrong page. This warns once, through `@flighthq/log`,
12
+ * for the repair it makes:
13
+ *
14
+ * **A glyph names a page the font does not have.** The glyph is clamped onto the primary page so it is
15
+ * still drawn, which is the right call for a font that must survive a bad asset — but it samples
16
+ * whatever happens to sit at those coordinates on page 0. That renders as a garbled glyph or a slice of
17
+ * a neighbour, which reads as an atlas-packing bug rather than the font-file defect it is.
18
+ *
19
+ * Pair with `explainBitmapFontGlyph` for a pull-style answer about one codepoint. Not importing this
20
+ * module costs production nothing: the messages and the `@flighthq/log` dependency live only here.
21
+ */
22
+ export function enableBitmapFontGuards() {
23
+ setBitmapFontGuard(warnOnBitmapFontRepair);
24
+ }
25
+ // Dispatched per reason rather than filtered, matching the glyph-atlas guard: a reason with no branch
26
+ // here logs nothing, which is the safe default when the core grows a repair this module has not been
27
+ // taught to describe. There is one reason today, so the no-match path is unreachable by construction and
28
+ // carries no test — a second reason adds its own branch rather than reusing this one's wording.
29
+ function warnOnBitmapFontRepair(reason, codepoint, page) {
30
+ if (reason !== 'page-out-of-range')
31
+ return;
32
+ const printable = `U+${codepoint.toString(16).toUpperCase().padStart(4, '0')}`;
33
+ logOnce('bitmapfont:page-out-of-range', LogLevel.Warn, {
34
+ message: `createBitmapFont: ${printable} names page ${page}, which this font does not have, so it was ` +
35
+ 'placed on page 0 and will sample whatever occupies those coordinates there. The font data is ' +
36
+ 'wrong, not the atlas — check the page index the exporter wrote.',
37
+ }, 'bitmapfont');
38
+ }
39
+ //# sourceMappingURL=enableBitmapFontGuards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableBitmapFontGuards.js","sourceRoot":"","sources":["../src/enableBitmapFontGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,kEAAkE;AAClE,MAAM,UAAU,uBAAuB;IACrC,kBAAkB,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB;IACpC,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;AAC7C,CAAC;AAED,sGAAsG;AACtG,qGAAqG;AACrG,yGAAyG;AACzG,gGAAgG;AAChG,SAAS,sBAAsB,CAAC,MAAc,EAAE,SAAiB,EAAE,IAAY;IAC7E,IAAI,MAAM,KAAK,mBAAmB;QAAE,OAAO;IAC3C,MAAM,SAAS,GAAG,KAAK,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IAC/E,OAAO,CACL,8BAA8B,EAC9B,QAAQ,CAAC,IAAI,EACb;QACE,OAAO,EACL,qBAAqB,SAAS,eAAe,IAAI,6CAA6C;YAC9F,+FAA+F;YAC/F,iEAAiE;KACpE,EACD,YAAY,CACb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { BitmapFont, BitmapFontGlyphExplanation } from '@flighthq/types/contract';
2
+ /** Reports why `getBitmapFontGlyph` returned null for `codepoint`, or why the glyph it returned will
3
+ * still draw nothing, as plain data.
4
+ *
5
+ * The lookup's null sentinel covers two situations a caller resolves differently — a font that does not
6
+ * cover the codepoint wants a fallback font, a font with no page images wants a repaired asset — and a
7
+ * third the sentinel cannot report at all, because it is not null: a glyph that exists and is zero-sized.
8
+ * A space is the ordinary case of that, and it is the one worth naming, since "the glyph is there and
9
+ * draws nothing on purpose" and "the glyph is missing" look identical from a blank screen.
10
+ *
11
+ * A pure read of the font's own maps: no rasterizing, no allocation beyond the returned record, and the
12
+ * same answer every time for a given font. Cheap enough to call per missing glyph while diagnosing. */
13
+ export declare function explainBitmapFontGlyph(font: Readonly<BitmapFont>, codepoint: number): BitmapFontGlyphExplanation;
14
+ //# sourceMappingURL=explainBitmapFontGlyph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explainBitmapFontGlyph.d.ts","sourceRoot":"","sources":["../src/explainBitmapFontGlyph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAEvF;;;;;;;;;;wGAUwG;AACxG,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,0BAA0B,CAehH"}
@@ -0,0 +1,27 @@
1
+ /** Reports why `getBitmapFontGlyph` returned null for `codepoint`, or why the glyph it returned will
2
+ * still draw nothing, as plain data.
3
+ *
4
+ * The lookup's null sentinel covers two situations a caller resolves differently — a font that does not
5
+ * cover the codepoint wants a fallback font, a font with no page images wants a repaired asset — and a
6
+ * third the sentinel cannot report at all, because it is not null: a glyph that exists and is zero-sized.
7
+ * A space is the ordinary case of that, and it is the one worth naming, since "the glyph is there and
8
+ * draws nothing on purpose" and "the glyph is missing" look identical from a blank screen.
9
+ *
10
+ * A pure read of the font's own maps: no rasterizing, no allocation beyond the returned record, and the
11
+ * same answer every time for a given font. Cheap enough to call per missing glyph while diagnosing. */
12
+ export function explainBitmapFontGlyph(font, codepoint) {
13
+ const pageCount = font.pages.length;
14
+ const glyph = font.glyphs.get(codepoint);
15
+ if (glyph === undefined) {
16
+ return { glyphHeight: 0, glyphWidth: 0, page: -1, pageCount, reason: 'no-glyph', renderable: false };
17
+ }
18
+ const shared = { glyphHeight: glyph.height, glyphWidth: glyph.width, page: glyph.page, pageCount };
19
+ // Checked before the size, because a font with no pages cannot draw even a well-formed glyph, and that
20
+ // is the more actionable of the two answers.
21
+ if (pageCount === 0)
22
+ return { ...shared, reason: 'no-pages', renderable: false };
23
+ if (glyph.width <= 0 || glyph.height <= 0)
24
+ return { ...shared, reason: 'empty-glyph', renderable: false };
25
+ return { ...shared, reason: 'ok', renderable: true };
26
+ }
27
+ //# sourceMappingURL=explainBitmapFontGlyph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explainBitmapFontGlyph.js","sourceRoot":"","sources":["../src/explainBitmapFontGlyph.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;wGAUwG;AACxG,MAAM,UAAU,sBAAsB,CAAC,IAA0B,EAAE,SAAiB;IAClF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAEzC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IACvG,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;IAEnG,uGAAuG;IACvG,6CAA6C;IAC7C,IAAI,SAAS,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IACjF,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAC1G,OAAO,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AACvD,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createBitmapFont, createGlyphSourceFromBitmapFont, getBitmapFontGlyph, getBitmapFontKerning, getBitmapFontMetrics, getBitmapFontPage, getBitmapFontPages, } from './contract';
1
+ export { createBitmapFont, createBitmapFontFromGlyphAtlas, createGlyphSourceFromBitmapFont, disableBitmapFontGuards, enableBitmapFontGuards, explainBitmapFontGlyph, getBitmapFontGlyph, getBitmapFontKerning, getBitmapFontMetrics, getBitmapFontPage, getBitmapFontPages, hasBitmapFontGlyph, summarizeBitmapFont, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,+BAA+B,EAC/B,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,8BAA8B,EAC9B,+BAA+B,EAC/B,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { createBitmapFont, createGlyphSourceFromBitmapFont, getBitmapFontGlyph, getBitmapFontKerning, getBitmapFontMetrics, getBitmapFontPage, getBitmapFontPages, } from './contract';
1
+ export { createBitmapFont, createBitmapFontFromGlyphAtlas, createGlyphSourceFromBitmapFont, disableBitmapFontGuards, enableBitmapFontGuards, explainBitmapFontGlyph, getBitmapFontGlyph, getBitmapFontKerning, getBitmapFontMetrics, getBitmapFontPage, getBitmapFontPages, hasBitmapFontGlyph, summarizeBitmapFont, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,+BAA+B,EAC/B,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,8BAA8B,EAC9B,+BAA+B,EAC/B,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { BitmapFont, BitmapFontSummary } from '@flighthq/types/contract';
2
+ /** Summarizes a font's size and coverage as plain data: glyph and kerning-pair counts, page count, the
3
+ * CPU-side bytes its page images occupy, and the codepoint range.
4
+ *
5
+ * Fonts are the largest fixed asset in a text-heavy scene and their cost is invisible from the API: a
6
+ * CJK-covering font carries hundreds of times the glyphs of a Latin one through exactly the same
7
+ * surface. This is what a budget check, a build report, or a debug overlay reads.
8
+ *
9
+ * The byte figure comes from `getTextureAtlasByteSize` rather than from width times height, which
10
+ * matters for two reasons: a compressed page costs its payload rather than four bytes per pixel, and a
11
+ * page that is unbound or already uploaded-and-released costs nothing. That makes `byteSize` a LOWER
12
+ * BOUND on a font mid-load rather than an estimate; `pageCount` is what shows the gap. */
13
+ export declare function summarizeBitmapFont(font: Readonly<BitmapFont>): BitmapFontSummary;
14
+ //# sourceMappingURL=summarizeBitmapFont.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summarizeBitmapFont.d.ts","sourceRoot":"","sources":["../src/summarizeBitmapFont.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9E;;;;;;;;;;2FAU2F;AAC3F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,iBAAiB,CAmBjF"}
@@ -0,0 +1,34 @@
1
+ import { getTextureAtlasByteSize } from '@flighthq/textureatlas/contract';
2
+ /** Summarizes a font's size and coverage as plain data: glyph and kerning-pair counts, page count, the
3
+ * CPU-side bytes its page images occupy, and the codepoint range.
4
+ *
5
+ * Fonts are the largest fixed asset in a text-heavy scene and their cost is invisible from the API: a
6
+ * CJK-covering font carries hundreds of times the glyphs of a Latin one through exactly the same
7
+ * surface. This is what a budget check, a build report, or a debug overlay reads.
8
+ *
9
+ * The byte figure comes from `getTextureAtlasByteSize` rather than from width times height, which
10
+ * matters for two reasons: a compressed page costs its payload rather than four bytes per pixel, and a
11
+ * page that is unbound or already uploaded-and-released costs nothing. That makes `byteSize` a LOWER
12
+ * BOUND on a font mid-load rather than an estimate; `pageCount` is what shows the gap. */
13
+ export function summarizeBitmapFont(font) {
14
+ let byteSize = 0;
15
+ for (const page of font.pages)
16
+ byteSize += getTextureAtlasByteSize(page);
17
+ let minCodepoint = -1;
18
+ let maxCodepoint = -1;
19
+ for (const codepoint of font.glyphs.keys()) {
20
+ if (minCodepoint < 0 || codepoint < minCodepoint)
21
+ minCodepoint = codepoint;
22
+ if (codepoint > maxCodepoint)
23
+ maxCodepoint = codepoint;
24
+ }
25
+ return {
26
+ byteSize,
27
+ glyphCount: font.glyphs.size,
28
+ kerningPairCount: font.kerning.size,
29
+ maxCodepoint,
30
+ minCodepoint,
31
+ pageCount: font.pages.length,
32
+ };
33
+ }
34
+ //# sourceMappingURL=summarizeBitmapFont.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summarizeBitmapFont.js","sourceRoot":"","sources":["../src/summarizeBitmapFont.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E;;;;;;;;;;2FAU2F;AAC3F,MAAM,UAAU,mBAAmB,CAAC,IAA0B;IAC5D,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK;QAAE,QAAQ,IAAI,uBAAuB,CAAC,IAAI,CAAC,CAAC;IAEzE,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC;IACtB,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC;IACtB,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QAC3C,IAAI,YAAY,GAAG,CAAC,IAAI,SAAS,GAAG,YAAY;YAAE,YAAY,GAAG,SAAS,CAAC;QAC3E,IAAI,SAAS,GAAG,YAAY;YAAE,YAAY,GAAG,SAAS,CAAC;IACzD,CAAC;IAED,OAAO;QACL,QAAQ;QACR,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;QAC5B,gBAAgB,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI;QACnC,YAAY;QACZ,YAAY;QACZ,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM;KAC7B,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@flighthq/bitmapfont",
3
- "version": "0.3.0-next.906.07cea63",
3
+ "version": "0.3.0",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
4
6
  "repository": {
5
7
  "type": "git",
6
8
  "url": "https://github.com/flighthq/flight.git",
@@ -36,10 +38,13 @@
36
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
39
  },
38
40
  "dependencies": {
39
- "@flighthq/textureatlas": "0.3.0-next.906.07cea63",
40
- "@flighthq/types": "0.3.0-next.906.07cea63"
41
+ "@flighthq/log": "0.3.0",
42
+ "@flighthq/texture": "0.3.0",
43
+ "@flighthq/textureatlas": "0.3.0",
44
+ "@flighthq/types": "0.3.0"
41
45
  },
42
46
  "devDependencies": {
47
+ "@flighthq/glyphatlas": "*",
43
48
  "typescript": "^5.3.0"
44
49
  },
45
50
  "description": "Static bitmap font — an immutable pre-baked atlas + glyph/kerning/metrics tables implementing the GlyphSource seam, the static sibling of the dynamic glyphatlas",
@@ -9,6 +9,10 @@ import {
9
9
  getBitmapFontMetrics,
10
10
  getBitmapFontPage,
11
11
  getBitmapFontPages,
12
+ hasBitmapFontGlyph,
13
+ packBitmapFontKerningKey,
14
+ setBitmapFontGuard,
15
+ unpackBitmapFontKerningKey,
12
16
  } from './bitmapFont';
13
17
 
14
18
  describe('createBitmapFont', () => {
@@ -82,6 +86,68 @@ describe('getBitmapFontKerning', () => {
82
86
  });
83
87
  });
84
88
 
89
+ describe('getBitmapFontKerning supplementary planes', () => {
90
+ // The old key was `(left << 16) | right`, and JavaScript's bitwise operators truncate to 32 bits, so a
91
+ // supplementary-plane codepoint did not overflow — it WRAPPED, landing on another pair's key. These
92
+ // are the two aliases that produced, verified arithmetically against the old packing before the fix.
93
+ const ALIASES = [
94
+ { name: 'U+10000 aliased U+0000', supplementary: 0x10000, basic: 0x0000 },
95
+ { name: 'U+1F600 aliased U+F600', supplementary: 0x1f600, basic: 0xf600 },
96
+ ];
97
+
98
+ for (const { name, supplementary, basic } of ALIASES) {
99
+ it(`keeps a supplementary-plane pair distinct where ${name}`, () => {
100
+ // Both pairs share a right glyph and differ only in the left, so a colliding key would make the
101
+ // second write overwrite the first and BOTH lookups would return -9.
102
+ const font = createBitmapFont({
103
+ ...sampleFontData(),
104
+ kerning: [
105
+ { amount: -4, left: basic, right: 65 },
106
+ { amount: -9, left: supplementary, right: 65 },
107
+ ],
108
+ });
109
+
110
+ expect(font.kerning.size).toBe(2);
111
+ expect(getBitmapFontKerning(font, basic, 65)).toBe(-4);
112
+ expect(getBitmapFontKerning(font, supplementary, 65)).toBe(-9);
113
+ });
114
+ }
115
+
116
+ it('keeps a supplementary-plane RIGHT glyph out of the left field', () => {
117
+ // The right codepoint occupied the low 16 bits, so anything above U+FFFF carried into the left
118
+ // glyph's field and reported another pair's adjustment.
119
+ const font = createBitmapFont({
120
+ ...sampleFontData(),
121
+ kerning: [
122
+ { amount: -3, left: 65, right: 0x10041 },
123
+ { amount: -7, left: 66, right: 0x41 },
124
+ ],
125
+ });
126
+
127
+ expect(getBitmapFontKerning(font, 65, 0x10041)).toBe(-3);
128
+ expect(getBitmapFontKerning(font, 66, 0x41)).toBe(-7);
129
+ });
130
+
131
+ it('addresses the whole Unicode range up to its last codepoint', () => {
132
+ const font = createBitmapFont({
133
+ ...sampleFontData(),
134
+ kerning: [{ amount: -5, left: 0x10ffff, right: 0x10ffff }],
135
+ });
136
+
137
+ // The largest key this packing produces; still an exact integer well inside the 2^53 range.
138
+ expect(getBitmapFontKerning(font, 0x10ffff, 0x10ffff)).toBe(-5);
139
+ expect(Number.isSafeInteger([...font.kerning.keys()][0])).toBe(true);
140
+ });
141
+
142
+ it('still returns 0 for a pair the font does not carry', () => {
143
+ // Guards the guard: the tests above would pass just as well against a packing that returned a
144
+ // distinct key for everything AND matched nothing.
145
+ const font = createBitmapFont(sampleFontData());
146
+
147
+ expect(getBitmapFontKerning(font, 0x10000, 65)).toBe(0);
148
+ });
149
+ });
150
+
85
151
  describe('getBitmapFontMetrics', () => {
86
152
  it('returns the font line metrics', () => {
87
153
  const font = createBitmapFont(sampleFontData());
@@ -103,16 +169,6 @@ describe('getBitmapFontPage', () => {
103
169
  });
104
170
  });
105
171
 
106
- describe('getBitmapFontPages', () => {
107
- it('returns the page-indexed atlas list', () => {
108
- const page0 = createTextureAtlas();
109
- const page1 = createTextureAtlas();
110
- const font = createBitmapFont({ ...sampleFontData(), pages: [page0, page1] });
111
-
112
- expect(getBitmapFontPages(font)).toEqual([page0, page1]);
113
- });
114
- });
115
-
116
172
  function sampleFontData(): BitmapFontData {
117
173
  return {
118
174
  glyphs: [
@@ -125,3 +181,147 @@ function sampleFontData(): BitmapFontData {
125
181
  pages: [createTextureAtlas()],
126
182
  };
127
183
  }
184
+
185
+ describe('getBitmapFontPages', () => {
186
+ it('returns the page-indexed atlas list', () => {
187
+ const page0 = createTextureAtlas();
188
+ const page1 = createTextureAtlas();
189
+ const font = createBitmapFont({ ...sampleFontData(), pages: [page0, page1] });
190
+
191
+ expect(getBitmapFontPages(font)).toEqual([page0, page1]);
192
+ });
193
+ });
194
+
195
+ describe('hasBitmapFontGlyph', () => {
196
+ it('reports coverage without the caller naming the null sentinel', () => {
197
+ const font = createBitmapFont(sampleFontData());
198
+
199
+ expect(hasBitmapFontGlyph(font, 65)).toBe(true);
200
+ expect(hasBitmapFontGlyph(font, 0x1f600)).toBe(false);
201
+ });
202
+
203
+ it('agrees with getBitmapFontGlyph on every codepoint it is asked about', () => {
204
+ const font = createBitmapFont(sampleFontData());
205
+
206
+ for (const codepoint of [65, 66, 86, 0, 67, 0x10000]) {
207
+ expect(hasBitmapFontGlyph(font, codepoint)).toBe(getBitmapFontGlyph(font, codepoint) !== null);
208
+ }
209
+ });
210
+ });
211
+
212
+ describe('packBitmapFontKerningKey', () => {
213
+ it('gives every distinct pair a distinct key across the full codepoint space', () => {
214
+ const pairs: readonly (readonly [number, number])[] = [
215
+ [65, 86],
216
+ [0, 65],
217
+ [0x10000, 65],
218
+ [0x1f600, 0xf600],
219
+ [0xf600, 0x1f600],
220
+ [0x10ffff, 0x10ffff],
221
+ ];
222
+
223
+ const keys = pairs.map(([left, right]) => packBitmapFontKerningKey(left, right));
224
+ expect(new Set(keys).size).toBe(pairs.length);
225
+ });
226
+
227
+ it('stays inside exact-integer range at the largest pair', () => {
228
+ // 0x10FFFF * 0x110000 + 0x10FFFF ≈ 1.24e12, well under 2^53 — the reason the key is arithmetic
229
+ // rather than a 32-bit shift.
230
+ const key = packBitmapFontKerningKey(0x10ffff, 0x10ffff);
231
+
232
+ expect(Number.isSafeInteger(key)).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe('setBitmapFontGuard', () => {
237
+ afterEach(() => {
238
+ setBitmapFontGuard(null);
239
+ });
240
+
241
+ it('reports the repair with the codepoint and the index that was out of range', () => {
242
+ // The seam, tested apart from the wording. enableBitmapFontGuards owns the message; this owns that
243
+ // the core calls out at all and hands over enough to identify the glyph — remove the call and the
244
+ // guard module has nothing to say however well written it is.
245
+ const calls: [string, number, number][] = [];
246
+ setBitmapFontGuard((reason, codepoint, page) => calls.push([reason, codepoint, page]));
247
+
248
+ createBitmapFont({
249
+ ...sampleFontData(),
250
+ glyphs: [{ advance: 9, bearingX: 1, bearingY: 8, codepoint: 0x41, height: 8, page: 7, width: 7, x: 0, y: 0 }],
251
+ });
252
+
253
+ expect(calls).toEqual([['page-out-of-range', 0x41, 7]]);
254
+ });
255
+
256
+ it('stays quiet for a glyph whose page exists', () => {
257
+ const calls: unknown[] = [];
258
+ setBitmapFontGuard((...args) => calls.push(args));
259
+
260
+ createBitmapFont(sampleFontData());
261
+
262
+ expect(calls).toEqual([]);
263
+ });
264
+
265
+ it('uninstalls the guard when passed null', () => {
266
+ const calls: unknown[] = [];
267
+ setBitmapFontGuard((...args) => calls.push(args));
268
+ setBitmapFontGuard(null);
269
+
270
+ createBitmapFont({
271
+ ...sampleFontData(),
272
+ glyphs: [{ advance: 9, bearingX: 1, bearingY: 8, codepoint: 0x41, height: 8, page: 7, width: 7, x: 0, y: 0 }],
273
+ });
274
+
275
+ expect(calls).toEqual([]);
276
+ });
277
+ });
278
+
279
+ describe('unpackBitmapFontKerningKey', () => {
280
+ it('recovers the pair a key was packed from', () => {
281
+ // The pairs are the ones a 16-bit inverse (`key >>> 16` / `key & 0xffff`) reads back wrong: an
282
+ // ordinary BMP pair whose key is not shift-shaped, a supplementary-plane left glyph, and a pair
283
+ // whose key leaves the 32-bit range the shift operators truncate to.
284
+ const pairs: readonly (readonly [number, number])[] = [
285
+ [65, 86],
286
+ [0, 0],
287
+ [0x41, 0x10ffff],
288
+ [0x1f600, 0x41],
289
+ [0x4e00, 0x4e8c],
290
+ [0x10ffff, 0x10ffff],
291
+ ];
292
+
293
+ for (const [left, right] of pairs) {
294
+ const key = packBitmapFontKerningKey(left, right);
295
+ expect(unpackBitmapFontKerningKey(key, { left: 0, right: 0 })).toEqual({ left, right });
296
+ }
297
+ });
298
+
299
+ it('recovers a pair whose key exceeds 2^32', () => {
300
+ const key = packBitmapFontKerningKey(0x1f600, 0x41);
301
+ expect(key).toBeGreaterThan(0x1_0000_0000);
302
+
303
+ const pair = unpackBitmapFontKerningKey(key, { left: 0, right: 0 });
304
+
305
+ expect(pair).toEqual({ left: 0x1f600, right: 0x41 });
306
+ // What the truncating inverse this replaced would have produced from the same key.
307
+ expect(pair.left).not.toBe(key >>> 16);
308
+ });
309
+
310
+ it('writes into the out object the caller supplied and returns it', () => {
311
+ const out = { left: -1, right: -1 };
312
+
313
+ const returned = unpackBitmapFontKerningKey(packBitmapFontKerningKey(0x10000, 0x41), out);
314
+
315
+ expect(returned).toBe(out);
316
+ expect(out).toEqual({ left: 0x10000, right: 0x41 });
317
+ });
318
+
319
+ it('reuses one out object across keys without carrying state between calls', () => {
320
+ const out = { left: 0, right: 0 };
321
+
322
+ unpackBitmapFontKerningKey(packBitmapFontKerningKey(0x10ffff, 0x10ffff), out);
323
+ unpackBitmapFontKerningKey(packBitmapFontKerningKey(0, 0), out);
324
+
325
+ expect(out).toEqual({ left: 0, right: 0 });
326
+ });
327
+ });
@@ -0,0 +1,81 @@
1
+ import { createGlyphAtlas, getGlyphAtlasEntry, setGlyphRasterizerBackend } from '@flighthq/glyphatlas/contract';
2
+ import type { GlyphRasterizerBackend } from '@flighthq/types/contract';
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+
5
+ import { getBitmapFontGlyph, getBitmapFontMetrics } from './bitmapFont';
6
+ import { createBitmapFontFromGlyphAtlas } from './bitmapFontFromGlyphAtlas';
7
+
8
+ function backendProducing(width: number, height: number): GlyphRasterizerBackend {
9
+ return {
10
+ measureMetrics: () => ({ ascent: 12, descent: 3, lineGap: 1 }),
11
+ rasterize: () => ({
12
+ advance: width,
13
+ bearingX: 1,
14
+ bearingY: 2,
15
+ height,
16
+ pixels: new Uint8ClampedArray(width * height * 4),
17
+ width,
18
+ }),
19
+ };
20
+ }
21
+
22
+ describe('createBitmapFontFromGlyphAtlas', () => {
23
+ afterEach(() => setGlyphRasterizerBackend(null));
24
+
25
+ function bakedFromTwoGlyphs() {
26
+ setGlyphRasterizerBackend(backendProducing(8, 8));
27
+ const atlas = createGlyphAtlas({ fontFamily: 'mock', fontSize: 16, height: 64, width: 64 });
28
+ getGlyphAtlasEntry(atlas, 65);
29
+ getGlyphAtlasEntry(atlas, 66);
30
+ return { atlas, font: createBitmapFontFromGlyphAtlas(atlas) };
31
+ }
32
+
33
+ it('carries every cached glyph across with its rect and pen data', () => {
34
+ const { atlas, font } = bakedFromTwoGlyphs();
35
+
36
+ const source = getGlyphAtlasEntry(atlas, 65)!;
37
+ const baked = getBitmapFontGlyph(font, 65)!;
38
+
39
+ expect(baked).not.toBeNull();
40
+ expect(baked.x).toBe(source.x);
41
+ expect(baked.y).toBe(source.y);
42
+ expect(baked.width).toBe(source.width);
43
+ expect(baked.advance).toBe(source.advance);
44
+ expect(baked.bearingY).toBe(source.bearingY);
45
+ expect(getBitmapFontGlyph(font, 66)).not.toBeNull();
46
+ });
47
+
48
+ it('carries the atlas metrics rather than re-deriving them', () => {
49
+ const { font } = bakedFromTwoGlyphs();
50
+ expect(getBitmapFontMetrics(font)).toEqual({ ascent: 12, descent: 3, lineGap: 1 });
51
+ });
52
+
53
+ // The page is CPU-only: the atlas bitmap is already a valid texture source under the flat model, so
54
+ // baking needs no GPU work and no readback.
55
+ it('builds a single page whose texture is sourced from the atlas bitmap', () => {
56
+ const { atlas, font } = bakedFromTwoGlyphs();
57
+
58
+ expect(font.pages).toHaveLength(1);
59
+ expect(font.pages[0]!.texture!.source).toBe(atlas.runtime.bitmap);
60
+ });
61
+
62
+ // A bake is a snapshot. Rasterizing more glyphs into the source atlas afterwards must not reach the
63
+ // font, or "bake" would just be another view of a live cache.
64
+ it('does not gain glyphs rasterized into the atlas after the bake', () => {
65
+ const { atlas, font } = bakedFromTwoGlyphs();
66
+
67
+ getGlyphAtlasEntry(atlas, 67);
68
+
69
+ expect(getBitmapFontGlyph(font, 67)).toBeNull();
70
+ });
71
+
72
+ it('bakes an empty atlas to a font with no glyphs rather than failing', () => {
73
+ setGlyphRasterizerBackend(backendProducing(8, 8));
74
+ const atlas = createGlyphAtlas({ fontFamily: 'mock', fontSize: 16, height: 64, width: 64 });
75
+
76
+ const font = createBitmapFontFromGlyphAtlas(atlas);
77
+
78
+ expect(getBitmapFontGlyph(font, 65)).toBeNull();
79
+ expect(font.pages).toHaveLength(1);
80
+ });
81
+ });
@@ -1,5 +1,5 @@
1
- import { createTextureAtlas } from '@flighthq/textureatlas/contract';
2
- import type { BitmapFontData, ImageResource } from '@flighthq/types/contract';
1
+ import { createTextureAtlas, createTextureAtlasFromImageResource } from '@flighthq/textureatlas/contract';
2
+ import type { BitmapFontData, Image } from '@flighthq/types/contract';
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
5
  import { createBitmapFont, getBitmapFontGlyph } from './bitmapFont';
@@ -19,8 +19,8 @@ describe('createGlyphSourceFromBitmapFont', () => {
19
19
  });
20
20
 
21
21
  it('pairs page 0 with the font atlas image and has no other page', () => {
22
- const image = {} as ImageResource;
23
- const font = createBitmapFont({ ...sampleFontData(), pages: [createTextureAtlas({ image })] });
22
+ const image = {} as Image;
23
+ const font = createBitmapFont({ ...sampleFontData(), pages: [createTextureAtlasFromImageResource(image)] });
24
24
  const source = createGlyphSourceFromBitmapFont(font);
25
25
 
26
26
  expect(source.getGlyphAtlasImage(0)).toBe(image);
@@ -29,11 +29,11 @@ describe('createGlyphSourceFromBitmapFont', () => {
29
29
  });
30
30
 
31
31
  it('resolves each page image of a multi-page font', () => {
32
- const image0 = {} as ImageResource;
33
- const image1 = {} as ImageResource;
32
+ const image0 = {} as Image;
33
+ const image1 = {} as Image;
34
34
  const font = createBitmapFont({
35
35
  ...sampleFontData(),
36
- pages: [createTextureAtlas({ image: image0 }), createTextureAtlas({ image: image1 })],
36
+ pages: [createTextureAtlasFromImageResource(image0), createTextureAtlasFromImageResource(image1)],
37
37
  });
38
38
  const source = createGlyphSourceFromBitmapFont(font);
39
39
 
@@ -0,0 +1,76 @@
1
+ import { setLogSink } from '@flighthq/log/contract';
2
+ import { createTextureAtlas } from '@flighthq/textureatlas/contract';
3
+ import type { BitmapFontData, LogEntry } from '@flighthq/types/contract';
4
+
5
+ import { createBitmapFont } from './bitmapFont';
6
+ import { disableBitmapFontGuards, enableBitmapFontGuards } from './enableBitmapFontGuards';
7
+
8
+ let entries: LogEntry[];
9
+
10
+ beforeEach(() => {
11
+ entries = [];
12
+ setLogSink((entry) => entries.push(entry));
13
+ });
14
+
15
+ afterEach(() => {
16
+ disableBitmapFontGuards();
17
+ setLogSink(null);
18
+ });
19
+
20
+ function messages(): string {
21
+ return entries.map((e) => String((e.data as { message?: unknown } | undefined)?.message ?? '')).join('\n');
22
+ }
23
+
24
+ function fontData(page: number, pages = 1): BitmapFontData {
25
+ return {
26
+ glyphs: [{ advance: 9, bearingX: 1, bearingY: 8, codepoint: 65, height: 8, page, width: 7, x: 0, y: 0 }],
27
+ metrics: { ascent: 8, descent: 2, lineGap: 1 },
28
+ pages: Array.from({ length: pages }, () => createTextureAtlas()),
29
+ };
30
+ }
31
+
32
+ describe('disableBitmapFontGuards', () => {
33
+ it('stops the warning a previously-installed guard would emit', () => {
34
+ enableBitmapFontGuards();
35
+ disableBitmapFontGuards();
36
+
37
+ createBitmapFont(fontData(4));
38
+
39
+ expect(entries).toEqual([]);
40
+ });
41
+ });
42
+
43
+ describe('enableBitmapFontGuards', () => {
44
+ it('warns which glyph and index are wrong, and that the font data is at fault', () => {
45
+ // ONE test for the whole message, deliberately. logOnce suppresses a key for the life of the
46
+ // PROCESS, so a second test asserting a second phrase would observe nothing and pass vacuously —
47
+ // the key is a single-use observation, so every assertion about it belongs in one place.
48
+ enableBitmapFontGuards();
49
+
50
+ createBitmapFont(fontData(4));
51
+
52
+ // The codepoint and the bad index, because the value of the warning is telling the reader which
53
+ // glyph and which index to look for in the font file.
54
+ expect(messages()).toContain('U+0041');
55
+ expect(messages()).toContain('page 4');
56
+ // And who is at fault: the failure renders as a garbled glyph, which reads as an atlas-packing bug.
57
+ // Naming the real culprit is why this warning exists rather than the clamp being left silent.
58
+ expect(messages()).toContain('font data is wrong, not the atlas');
59
+ });
60
+
61
+ it('stays silent for a glyph whose page exists', () => {
62
+ // Guards the guard: every assertion above would pass just as well against a guard that fired
63
+ // unconditionally, which would make the warning noise on every well-formed font.
64
+ enableBitmapFontGuards();
65
+
66
+ createBitmapFont(fontData(1, 2));
67
+
68
+ expect(entries).toEqual([]);
69
+ });
70
+
71
+ it('stays silent when no guard is installed at all', () => {
72
+ createBitmapFont(fontData(4));
73
+
74
+ expect(entries).toEqual([]);
75
+ });
76
+ });
@@ -0,0 +1,73 @@
1
+ import { createTextureAtlas } from '@flighthq/textureatlas/contract';
2
+ import type { BitmapFontData } from '@flighthq/types/contract';
3
+
4
+ import { createBitmapFont } from './bitmapFont';
5
+ import { explainBitmapFontGlyph } from './explainBitmapFontGlyph';
6
+
7
+ function fontData(over: Partial<BitmapFontData> = {}): BitmapFontData {
8
+ return {
9
+ glyphs: [
10
+ { advance: 9, bearingX: 1, bearingY: 8, codepoint: 65, height: 8, width: 7, x: 0, y: 0 },
11
+ { advance: 4, bearingX: 0, bearingY: 0, codepoint: 32, height: 0, width: 0, x: 0, y: 0 },
12
+ ],
13
+ metrics: { ascent: 8, descent: 2, lineGap: 1 },
14
+ pages: [createTextureAtlas()],
15
+ ...over,
16
+ };
17
+ }
18
+
19
+ describe('explainBitmapFontGlyph', () => {
20
+ it('reports ok for a glyph that will draw', () => {
21
+ const font = createBitmapFont(fontData());
22
+
23
+ expect(explainBitmapFontGlyph(font, 65)).toEqual({
24
+ glyphHeight: 8,
25
+ glyphWidth: 7,
26
+ page: 0,
27
+ pageCount: 1,
28
+ reason: 'ok',
29
+ renderable: true,
30
+ });
31
+ });
32
+
33
+ it('distinguishes a codepoint the font does not cover', () => {
34
+ const font = createBitmapFont(fontData());
35
+
36
+ expect(explainBitmapFontGlyph(font, 0x1f600)).toEqual({
37
+ glyphHeight: 0,
38
+ glyphWidth: 0,
39
+ // -1 rather than 0: there is no glyph, so there is no page it resolved to, and reporting 0 would
40
+ // read as "page zero" to a caller checking which page to upload.
41
+ page: -1,
42
+ pageCount: 1,
43
+ reason: 'no-glyph',
44
+ renderable: false,
45
+ });
46
+ });
47
+
48
+ it('distinguishes a zero-sized glyph the font does carry', () => {
49
+ // A space. The lookup returns a glyph, so the null sentinel cannot report this at all — which is the
50
+ // reason it is worth a distinct reason: drew-nothing-on-purpose and drew-nothing-by-mistake look
51
+ // identical from a blank screen.
52
+ const font = createBitmapFont(fontData());
53
+
54
+ const explanation = explainBitmapFontGlyph(font, 32);
55
+ expect(explanation.reason).toBe('empty-glyph');
56
+ expect(explanation.renderable).toBe(false);
57
+ });
58
+
59
+ it('reports a font with no page images ahead of the glyph size', () => {
60
+ // Both faults are present on this glyph — no pages AND it is well-formed — and no-pages is the more
61
+ // actionable answer, so the order is deliberate rather than incidental.
62
+ const font = createBitmapFont(fontData({ pages: [] }));
63
+
64
+ expect(explainBitmapFontGlyph(font, 65).reason).toBe('no-pages');
65
+ expect(explainBitmapFontGlyph(font, 65).pageCount).toBe(0);
66
+ });
67
+
68
+ it('reports no-pages rather than empty-glyph when both apply', () => {
69
+ const font = createBitmapFont(fontData({ pages: [] }));
70
+
71
+ expect(explainBitmapFontGlyph(font, 32).reason).toBe('no-pages');
72
+ });
73
+ });
@@ -0,0 +1,86 @@
1
+ import { createTexture } from '@flighthq/texture/contract';
2
+ import { createTextureAtlas } from '@flighthq/textureatlas/contract';
3
+ import type { Bitmap, BitmapFontData, TextureAtlas } from '@flighthq/types/contract';
4
+ import { BitmapTextureSourceKind } from '@flighthq/types/contract';
5
+
6
+ import { createBitmapFont } from './bitmapFont';
7
+ import { summarizeBitmapFont } from './summarizeBitmapFont';
8
+
9
+ // A page whose CPU-side image data is `bytes` long. The summary reports the byte footprint of the
10
+ // actual data rather than width times height, so the fixture supplies data rather than dimensions.
11
+ function pageOfBytes(bytes: number): TextureAtlas {
12
+ const image = {
13
+ alphaType: 'straight',
14
+ gamut: 'srgb',
15
+ data: new Uint8ClampedArray(bytes),
16
+ format: 'rgba8unorm',
17
+ height: 1,
18
+ kind: BitmapTextureSourceKind,
19
+ version: 0,
20
+ width: bytes / 4,
21
+ } as unknown as Bitmap;
22
+ return createTextureAtlas({ texture: createTexture({ dimension: '2d', source: image }) });
23
+ }
24
+
25
+ function fontData(over: Partial<BitmapFontData> = {}): BitmapFontData {
26
+ return {
27
+ glyphs: [
28
+ { advance: 9, bearingX: 1, bearingY: 8, codepoint: 65, height: 8, width: 7, x: 0, y: 0 },
29
+ { advance: 9, bearingX: 1, bearingY: 8, codepoint: 0x1f600, height: 8, width: 7, x: 8, y: 0 },
30
+ ],
31
+ kerning: [{ amount: -2, left: 65, right: 0x1f600 }],
32
+ metrics: { ascent: 8, descent: 2, lineGap: 1 },
33
+ pages: [pageOfBytes(64 * 32 * 4)],
34
+ ...over,
35
+ };
36
+ }
37
+
38
+ describe('summarizeBitmapFont', () => {
39
+ it('counts glyphs, kerning pairs, pages, pixels and bytes', () => {
40
+ const font = createBitmapFont(fontData());
41
+
42
+ expect(summarizeBitmapFont(font)).toEqual({
43
+ byteSize: 64 * 32 * 4,
44
+ glyphCount: 2,
45
+ kerningPairCount: 1,
46
+ maxCodepoint: 0x1f600,
47
+ minCodepoint: 65,
48
+ pageCount: 1,
49
+ });
50
+ });
51
+
52
+ it('sums every page rather than reporting only the first', () => {
53
+ const font = createBitmapFont(fontData({ pages: [pageOfBytes(2048), pageOfBytes(512)] }));
54
+
55
+ const summary = summarizeBitmapFont(font);
56
+ expect(summary.pageCount).toBe(2);
57
+ expect(summary.byteSize).toBe(2560);
58
+ });
59
+
60
+ it('treats an unresolved page as contributing nothing, making the size a lower bound', () => {
61
+ // A page whose texture has not loaded yet. Counting it as zero is what makes the figure a floor
62
+ // rather than an estimate, and the page still shows in pageCount so the gap is visible.
63
+ const font = createBitmapFont(fontData({ pages: [pageOfBytes(2048), createTextureAtlas()] }));
64
+
65
+ const summary = summarizeBitmapFont(font);
66
+ expect(summary.pageCount).toBe(2);
67
+ expect(summary.byteSize).toBe(2048);
68
+ });
69
+
70
+ it('reports -1 for both ends of the codepoint range of an empty font', () => {
71
+ // 0 would be a lie: U+0000 is a real codepoint, so a caller cannot tell an empty font from one
72
+ // carrying only NUL.
73
+ const font = createBitmapFont(fontData({ glyphs: [], kerning: [] }));
74
+
75
+ const summary = summarizeBitmapFont(font);
76
+ expect(summary.minCodepoint).toBe(-1);
77
+ expect(summary.maxCodepoint).toBe(-1);
78
+ expect(summary.glyphCount).toBe(0);
79
+ });
80
+
81
+ it('reports the range across supplementary-plane glyphs', () => {
82
+ const font = createBitmapFont(fontData());
83
+
84
+ expect(summarizeBitmapFont(font).maxCodepoint).toBe(0x1f600);
85
+ });
86
+ });