@forgeax/engine-font 0.1.4 → 0.1.7

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.
package/src/cli-font.ts CHANGED
@@ -225,87 +225,6 @@ export interface BakeResult {
225
225
  readonly sidecarPath: string;
226
226
  }
227
227
 
228
- function malformedAtlasTextureCause(atlas: BakeAtlas): string | undefined {
229
- const texture = atlas.texture;
230
- const textureSize = atlas.textureSize;
231
- if (
232
- !Number.isSafeInteger(texture.width) ||
233
- texture.width <= 0 ||
234
- !Number.isSafeInteger(texture.height) ||
235
- texture.height <= 0
236
- ) {
237
- return 'malformed-atlas: texture dimensions must be positive finite integers';
238
- }
239
- if (
240
- !Number.isSafeInteger(textureSize[0]) ||
241
- textureSize[0] <= 0 ||
242
- !Number.isSafeInteger(textureSize[1]) ||
243
- textureSize[1] <= 0
244
- ) {
245
- return 'malformed-atlas: textureSize dimensions must be positive finite integers';
246
- }
247
- if (texture.width !== textureSize[0] || texture.height !== textureSize[1]) {
248
- return 'malformed-atlas: texture dimensions must match textureSize';
249
- }
250
- const expectedBytes = texture.width * texture.height * 4;
251
- if (!(texture.data instanceof Uint8Array) || texture.data.length !== expectedBytes) {
252
- return 'malformed-atlas: RGBA data length must equal width * height * 4';
253
- }
254
- return undefined;
255
- }
256
-
257
- function malformedGlyphCause(atlas: BakeAtlas): string | undefined {
258
- if (
259
- !Number.isFinite(atlas.metrics.lineHeight) ||
260
- !Number.isFinite(atlas.metrics.ascender) ||
261
- !Number.isFinite(atlas.fieldRange)
262
- ) {
263
- return 'malformed-glyph: common metrics must be finite';
264
- }
265
-
266
- const [atlasWidth, atlasHeight] = atlas.textureSize;
267
- const seenUnicode = new Set<number>();
268
- for (let index = 0; index < atlas.glyphs.length; index += 1) {
269
- const glyph = atlas.glyphs[index];
270
- if (glyph === undefined) {
271
- return `malformed-glyph: missing glyph record at index ${index}`;
272
- }
273
- if (!Number.isSafeInteger(glyph.unicode) || glyph.unicode < 0 || glyph.unicode > 0x10ffff) {
274
- return `malformed-glyph: invalid unicode identity at index ${index}`;
275
- }
276
- if (seenUnicode.has(glyph.unicode)) {
277
- return `malformed-glyph: duplicate unicode identity at index ${index}`;
278
- }
279
- seenUnicode.add(glyph.unicode);
280
-
281
- if (
282
- !Number.isFinite(glyph.advance) ||
283
- !Number.isFinite(glyph.xoffset) ||
284
- !Number.isFinite(glyph.yoffset)
285
- ) {
286
- return `malformed-glyph: non-finite metrics at index ${index}`;
287
- }
288
-
289
- const [x, y] = glyph.atlasPosition;
290
- const [width, height] = glyph.atlasSize;
291
- if (
292
- !Number.isSafeInteger(x) ||
293
- !Number.isSafeInteger(y) ||
294
- !Number.isSafeInteger(width) ||
295
- !Number.isSafeInteger(height) ||
296
- x < 0 ||
297
- y < 0 ||
298
- width < 0 ||
299
- height < 0 ||
300
- x + width > atlasWidth ||
301
- y + height > atlasHeight
302
- ) {
303
- return `malformed-glyph: atlas region out of bounds at index ${index}`;
304
- }
305
- }
306
- return undefined;
307
- }
308
-
309
228
  /**
310
229
  * Bake an MSDF atlas + glyph-metrics sidecar from a TTF.
311
230
  *
@@ -323,17 +242,7 @@ export async function bakeFont(
323
242
  outDir: string,
324
243
  generatorFactory: () => Promise<MsdfGenerator>,
325
244
  ): Promise<BakeResult> {
326
- let ttf: Buffer;
327
- try {
328
- ttf = await readFile(ttfPath);
329
- } catch (e) {
330
- throw new FontError({
331
- code: 'bake-failed',
332
- expected: 'a readable TrueType source file',
333
- hint: 'repair the source path or filesystem access, then retry the bake',
334
- detail: { path: ttfPath, cause: e instanceof Error ? e.message : String(e) },
335
- });
336
- }
245
+ const ttf = await readFile(ttfPath);
337
246
  const ttfBytes = new Uint8Array(ttf.buffer, ttf.byteOffset, ttf.byteLength);
338
247
  if (!isSupportedFontMagic(ttfBytes)) {
339
248
  throw new FontError({
@@ -344,104 +253,33 @@ export async function bakeFont(
344
253
  });
345
254
  }
346
255
 
347
- try {
348
- await mkdir(outDir, { recursive: true });
349
- } catch (e) {
350
- throw new FontError({
351
- code: 'bake-failed',
352
- expected: 'a writable output directory',
353
- hint: 'repair the output path or filesystem access, then retry the bake',
354
- detail: { path: outDir, cause: e instanceof Error ? e.message : String(e) },
355
- });
356
- }
357
-
358
- let atlas: BakeAtlas | undefined;
256
+ let atlas: BakeAtlas;
359
257
  let generator: MsdfGenerator | undefined;
360
- let primaryFailure: unknown;
361
- let primaryFailed = false;
362
- let disposeFailure: unknown;
363
- let disposeFailed = false;
364
258
  try {
365
259
  generator = await generatorFactory();
366
260
  atlas = await generator.generateAtlas(ttfBytes);
367
261
  } catch (e) {
368
- primaryFailed = true;
369
- primaryFailure = e;
370
- } finally {
371
- if (generator !== undefined) {
372
- try {
373
- await generator.dispose();
374
- } catch (e) {
375
- disposeFailed = true;
376
- disposeFailure = e;
377
- }
378
- }
379
- }
380
-
381
- if (primaryFailed) {
382
262
  throw new FontError({
383
263
  code: 'bake-failed',
384
264
  expected: '@zappar/msdf-generator to produce an MSDF atlas',
385
265
  hint: 'the MSDF generator threw -- a Web Worker + wasm host is required (a plain Node process reports "Worker is not defined"); run the bake in a Worker-capable environment',
386
- detail: {
387
- cause: primaryFailure instanceof Error ? primaryFailure.message : String(primaryFailure),
388
- },
389
- });
390
- }
391
- if (disposeFailed) {
392
- throw new FontError({
393
- code: 'bake-failed',
394
- expected: 'the MSDF generator to dispose cleanly',
395
- hint: 'repair the MSDF generator lifecycle, then retry the bake',
396
- detail: {
397
- cause: disposeFailure instanceof Error ? disposeFailure.message : String(disposeFailure),
398
- },
399
- });
400
- }
401
- if (atlas === undefined) {
402
- throw new FontError({
403
- code: 'bake-failed',
404
- expected: '@zappar/msdf-generator to produce an MSDF atlas',
405
- hint: 'repair the MSDF generator lifecycle, then retry the bake',
406
- detail: { cause: 'the MSDF generator produced no atlas' },
266
+ detail: { cause: e instanceof Error ? e.message : String(e) },
407
267
  });
268
+ } finally {
269
+ if (generator !== undefined) {
270
+ await generator.dispose().catch(() => undefined);
271
+ }
408
272
  }
409
273
 
274
+ await mkdir(outDir, { recursive: true });
410
275
  const base = basename(ttfPath, extname(ttfPath));
411
276
  const atlasName = `${base}.atlas.png`;
412
277
  const atlasPath = join(outDir, atlasName);
413
278
  const sidecarPath = join(outDir, `${base}.meta.json`);
414
- const malformedCause = malformedAtlasTextureCause(atlas) ?? malformedGlyphCause(atlas);
415
- if (malformedCause !== undefined) {
416
- throw new FontError({
417
- code: 'bake-failed',
418
- expected: 'a valid MSDF atlas texture and glyph metrics',
419
- hint: 'repair the MSDF generator atlas and glyph output, then retry the bake',
420
- detail: { cause: malformedCause },
421
- });
422
- }
423
279
  const png = encodePng(atlas.texture.width, atlas.texture.height, atlas.texture.data);
424
- try {
425
- await writeFile(atlasPath, png);
426
- } catch (e) {
427
- throw new FontError({
428
- code: 'bake-failed',
429
- expected: 'a writable atlas output path',
430
- hint: 'repair the atlas output path or filesystem access, then retry the bake',
431
- detail: { path: atlasPath, cause: e instanceof Error ? e.message : String(e) },
432
- });
433
- }
280
+ await writeFile(atlasPath, png);
434
281
  const sidecar = atlasToSidecar(atlas, atlasName);
435
- try {
436
- await writeFile(sidecarPath, `${JSON.stringify(sidecar, null, 2)}\n`);
437
- } catch (e) {
438
- throw new FontError({
439
- code: 'bake-failed',
440
- expected: 'a writable sidecar output path',
441
- hint: 'repair the sidecar output path or filesystem access, then retry the bake',
442
- detail: { path: sidecarPath, cause: e instanceof Error ? e.message : String(e) },
443
- });
444
- }
282
+ await writeFile(sidecarPath, `${JSON.stringify(sidecar, null, 2)}\n`);
445
283
  return { atlasPath, sidecarPath };
446
284
  }
447
285
 
@@ -31,17 +31,15 @@
31
31
  // instead of the real wasm generator; production import calls fall through to
32
32
  // the real @zappar factory.
33
33
 
34
- import {
35
- type FontAsset,
36
- type GlyphMetric,
37
- IMPORT_ERROR_HINTS,
38
- type ImportContext,
39
- ImportError,
40
- type ImportedAsset,
41
- type Importer,
42
- type ImportResult,
43
- type SamplerAsset,
44
- type TextureAsset,
34
+ import type {
35
+ FontAsset,
36
+ GlyphMetric,
37
+ ImportContext,
38
+ ImportedAsset,
39
+ Importer,
40
+ ImportResult,
41
+ SamplerAsset,
42
+ TextureAsset,
45
43
  } from '@forgeax/engine-types';
46
44
  import type { BakeAtlas, MsdfGenerator } from './cli-font.js';
47
45
  import { realGeneratorFactory } from './cli-font.js';
@@ -56,41 +54,6 @@ export function fontOutputSourceKeys(): readonly string[] {
56
54
  return ['texture', 'sampler', 'font'].map((kind) => sourceKeyForFontOutput(kind) as string);
57
55
  }
58
56
 
59
- const FONT_REQUIRED_KINDS = ['texture', 'sampler', 'font'] as const;
60
- const FONT_REQUIRED_TOPOLOGY = 'exactly one texture, one sampler, and one font subAsset';
61
-
62
- function validateRequiredFontSubAssets(ctx: ImportContext): ImportError | undefined {
63
- const requiredKinds = new Set<string>(FONT_REQUIRED_KINDS);
64
- const counts = new Map<string, number>();
65
- for (const subAsset of ctx.subAssets) {
66
- if (!requiredKinds.has(subAsset.kind)) continue;
67
- counts.set(subAsset.kind, (counts.get(subAsset.kind) ?? 0) + 1);
68
- }
69
-
70
- const actual = FONT_REQUIRED_KINDS.map((kind) => `${kind}=${counts.get(kind) ?? 0}`).join(', ');
71
- if (FONT_REQUIRED_KINDS.every((kind) => counts.get(kind) === 1)) return undefined;
72
-
73
- return new ImportError({
74
- code: 'source-validation-failed',
75
- expected: FONT_REQUIRED_TOPOLOGY,
76
- hint: IMPORT_ERROR_HINTS['source-validation-failed'],
77
- detail: {
78
- diagnostics: [
79
- {
80
- code: 'font-required-subasset-topology',
81
- severity: 'error',
82
- sourcePath: `${ctx.source}#subAssets`,
83
- sourceRange: { start: 0, end: 0, line: 1, column: 1 },
84
- rule: 'font-required-subasset-topology',
85
- expected: FONT_REQUIRED_TOPOLOGY,
86
- actual,
87
- hint: 'Declare exactly one texture, one sampler, and one font subAsset before importing.',
88
- },
89
- ],
90
- },
91
- });
92
- }
93
-
94
57
  /** Map the @zappar atlas glyphs into the FontAsset glyph-metrics record. */
95
58
  function atlasGlyphsToMetrics(atlas: BakeAtlas): Record<number, GlyphMetric> {
96
59
  const glyphs: Record<number, GlyphMetric> = {};
@@ -148,23 +111,11 @@ function makeAtlasSampler(): SamplerAsset {
148
111
  }
149
112
 
150
113
  async function importFont(ctx: ImportContext): Promise<ImportResult> {
151
- const topologyError = validateRequiredFontSubAssets(ctx);
152
- if (topologyError !== undefined) return { ok: false, error: topologyError };
153
-
154
114
  const read = await ctx.readSource();
155
115
  if (!read.ok) {
156
- return {
157
- ok: false,
158
- error: new ImportError({
159
- code: 'source-read-failed',
160
- expected: `readable source file at meta.source "${ctx.source}"`,
161
- hint: IMPORT_ERROR_HINTS['source-read-failed'],
162
- detail: {
163
- source: ctx.source,
164
- reason: read.error instanceof Error ? read.error.message : String(read.error),
165
- },
166
- }),
167
- };
116
+ throw new Error(
117
+ `fontImporter: readSource failed: ${read.error instanceof Error ? read.error.message : String(read.error)}`,
118
+ );
168
119
  }
169
120
 
170
121
  const factory =