@forgeax/engine-font 0.1.2
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/LICENSE +202 -0
- package/README.md +100 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/font-importer.test.d.ts +2 -0
- package/dist/__tests__/font-importer.test.d.ts.map +1 -0
- package/dist/__tests__/font-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/font-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/font.unit.test.d.ts +2 -0
- package/dist/__tests__/font.unit.test.d.ts.map +1 -0
- package/dist/cli-font.d.ts +96 -0
- package/dist/cli-font.d.ts.map +1 -0
- package/dist/cli-font.mjs +465 -0
- package/dist/cli-font.mjs.map +1 -0
- package/dist/font-importer.d.ts +18 -0
- package/dist/font-importer.d.ts.map +1 -0
- package/dist/font-importer.mjs +637 -0
- package/dist/font-importer.mjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +54 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-msdf-worker.mjs +35 -0
- package/dist/node-msdf-worker.mjs.map +1 -0
- package/dist/node-worker-adapter.d.ts +17 -0
- package/dist/node-worker-adapter.d.ts.map +1 -0
- package/dist/runtime/font-decoder.d.ts +3 -0
- package/dist/runtime/font-decoder.d.ts.map +1 -0
- package/package.json +80 -0
- package/src/__tests__/font-importer.test.ts +24 -0
- package/src/__tests__/font-local-artifacts.test.ts +280 -0
- package/src/__tests__/font.unit.test.ts +1039 -0
- package/src/cli-font.ts +634 -0
- package/src/font-importer.ts +254 -0
- package/src/index.ts +7 -0
- package/src/node-msdf-worker.mjs +38 -0
- package/src/node-worker-adapter.ts +41 -0
- package/src/runtime/font-decoder.ts +70 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// font-importer.ts - the build-time fontImporter (feat-20260603-asset-import-loader-injection M3 / w24).
|
|
2
|
+
//
|
|
3
|
+
// The `{ key: 'font', import }` Importer the @forgeax/engine-import runner
|
|
4
|
+
// dispatches a `*.meta.json` with `importer: 'font'` to. It absorbs the MSDF
|
|
5
|
+
// bake that previously lived only behind the `forgeax-engine-remote-font
|
|
6
|
+
// bake` CLI: read the `.ttf` source -> @zappar/msdf-generator atlas ->
|
|
7
|
+
// (a) one atlas `TextureAsset` ImportedAsset (the RGBA MSDF atlas, kind
|
|
8
|
+
// 'texture') under the declared `kind: 'texture'` sub-asset GUID, (b) one
|
|
9
|
+
// sampler ImportedAsset under the declared `kind: 'sampler'` sub-asset GUID,
|
|
10
|
+
// and (c) one font glyph-metrics ImportedAsset (kind 'font') under the
|
|
11
|
+
// declared `kind: 'font'` sub-asset GUID. The font carries the BMFont ->
|
|
12
|
+
// FontAsset glyph map + the common block + atlas/sampler GUID refs.
|
|
13
|
+
//
|
|
14
|
+
// Build-time-only boundary (AC-18 / requirements callout): @zappar/msdf-generator
|
|
15
|
+
// is a generator dependency that MUST stay out of the runtime bundle. The
|
|
16
|
+
// runtime today statically imports ZERO @forgeax/engine-font and ZERO @zappar
|
|
17
|
+
// symbols (research Finding 7); this importer keeps that invariant. fontImporter
|
|
18
|
+
// is a NODE-ONLY sub-export (`@forgeax/engine-font/font-importer`,
|
|
19
|
+
// `default: null` under browser conditions); the generator is dynamically
|
|
20
|
+
// imported (cli-font realGeneratorFactory) so even the build-time graph only
|
|
21
|
+
// pays the wasm load cost when a font is actually baked. The runtime fontLoader
|
|
22
|
+
// (M1 w6) reads an already-baked atlas DDC and has no bake dependency.
|
|
23
|
+
//
|
|
24
|
+
// GUID import-stable iron law: produced GUIDs come from `ctx.subAssets[]`. The
|
|
25
|
+
// font sidecar declares `texture`, `sampler`, and `font` sub-assets; this
|
|
26
|
+
// importer maps the bake output onto those declared GUIDs and stamps nothing
|
|
27
|
+
// of its own.
|
|
28
|
+
//
|
|
29
|
+
// `ctx.importSettings` may inject a test `generatorFactory` (an
|
|
30
|
+
// `() => Promise<MsdfGenerator>`) so unit tests drive the bake with a mock
|
|
31
|
+
// instead of the real wasm generator; production import calls fall through to
|
|
32
|
+
// the real @zappar factory.
|
|
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,
|
|
45
|
+
} from '@forgeax/engine-types';
|
|
46
|
+
import type { BakeAtlas, MsdfGenerator } from './cli-font.js';
|
|
47
|
+
import { realGeneratorFactory } from './cli-font.js';
|
|
48
|
+
|
|
49
|
+
/** Stable semantic identities for the three writable font outputs. */
|
|
50
|
+
export function sourceKeyForFontOutput(kind: string): string | undefined {
|
|
51
|
+
const normalizedKind = kind.trim();
|
|
52
|
+
return normalizedKind.length === 0 ? undefined : `font:${normalizedKind}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function fontOutputSourceKeys(): readonly string[] {
|
|
56
|
+
return ['texture', 'sampler', 'font'].map((kind) => sourceKeyForFontOutput(kind) as string);
|
|
57
|
+
}
|
|
58
|
+
|
|
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
|
+
/** Map the @zappar atlas glyphs into the FontAsset glyph-metrics record. */
|
|
95
|
+
function atlasGlyphsToMetrics(atlas: BakeAtlas): Record<number, GlyphMetric> {
|
|
96
|
+
const glyphs: Record<number, GlyphMetric> = {};
|
|
97
|
+
for (const g of atlas.glyphs) {
|
|
98
|
+
glyphs[g.unicode] = {
|
|
99
|
+
advance: g.advance,
|
|
100
|
+
bearingX: g.xoffset,
|
|
101
|
+
bearingY: g.yoffset,
|
|
102
|
+
size: { w: g.atlasSize[0], h: g.atlasSize[1] },
|
|
103
|
+
region: {
|
|
104
|
+
x: g.atlasPosition[0],
|
|
105
|
+
y: g.atlasPosition[1],
|
|
106
|
+
w: g.atlasSize[0],
|
|
107
|
+
h: g.atlasSize[1],
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return glyphs;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function makeAtlasTexture(atlas: BakeAtlas): TextureAsset {
|
|
115
|
+
return {
|
|
116
|
+
kind: 'texture',
|
|
117
|
+
width: atlas.texture.width,
|
|
118
|
+
height: atlas.texture.height,
|
|
119
|
+
// MSDF atlas is linear-space RGBA8 (signed-distance channels, never gamma).
|
|
120
|
+
format: 'rgba8unorm',
|
|
121
|
+
data: atlas.texture.data,
|
|
122
|
+
colorSpace: 'linear',
|
|
123
|
+
mipmap: false,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function makeFontCommon(atlas: BakeAtlas): FontAsset['common'] {
|
|
128
|
+
return {
|
|
129
|
+
lineHeight: atlas.metrics.lineHeight,
|
|
130
|
+
base: atlas.metrics.ascender,
|
|
131
|
+
distanceRange: atlas.fieldRange,
|
|
132
|
+
pxRange: atlas.fieldRange,
|
|
133
|
+
atlasWidth: atlas.textureSize[0],
|
|
134
|
+
atlasHeight: atlas.textureSize[1],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function makeAtlasSampler(): SamplerAsset {
|
|
139
|
+
return {
|
|
140
|
+
kind: 'sampler',
|
|
141
|
+
addressModeU: 'clamp-to-edge',
|
|
142
|
+
addressModeV: 'clamp-to-edge',
|
|
143
|
+
addressModeW: 'clamp-to-edge',
|
|
144
|
+
magFilter: 'linear',
|
|
145
|
+
minFilter: 'linear',
|
|
146
|
+
mipmapFilter: 'nearest',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function importFont(ctx: ImportContext): Promise<ImportResult> {
|
|
151
|
+
const topologyError = validateRequiredFontSubAssets(ctx);
|
|
152
|
+
if (topologyError !== undefined) return { ok: false, error: topologyError };
|
|
153
|
+
|
|
154
|
+
const read = await ctx.readSource();
|
|
155
|
+
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
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const factory =
|
|
171
|
+
(ctx.importSettings.generatorFactory as (() => Promise<MsdfGenerator>) | undefined) ??
|
|
172
|
+
realGeneratorFactory;
|
|
173
|
+
const generator = await factory();
|
|
174
|
+
let atlas: BakeAtlas;
|
|
175
|
+
try {
|
|
176
|
+
atlas = await generator.generateAtlas(read.value);
|
|
177
|
+
} finally {
|
|
178
|
+
await generator.dispose().catch(() => undefined);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const atlasSub = ctx.subAssets.find((s) => s.kind === 'texture');
|
|
182
|
+
const samplerSub = ctx.subAssets.find((s) => s.kind === 'sampler');
|
|
183
|
+
const fontSub = ctx.subAssets.find((s) => s.kind === 'font');
|
|
184
|
+
const out: ImportedAsset[] = [];
|
|
185
|
+
if (atlasSub !== undefined) {
|
|
186
|
+
out.push({
|
|
187
|
+
guid: atlasSub.guid,
|
|
188
|
+
kind: 'texture',
|
|
189
|
+
payload: makeAtlasTexture(atlas),
|
|
190
|
+
refs: [],
|
|
191
|
+
artifacts: {
|
|
192
|
+
atlas: {
|
|
193
|
+
// Pack v2's runtime texture loader consumes non-Basis artifacts as
|
|
194
|
+
// raw pixels. Keep the baked RGBA8 MSDF atlas in that form here;
|
|
195
|
+
// the CLI still emits a PNG for standalone bake output.
|
|
196
|
+
mediaType: 'application/octet-stream',
|
|
197
|
+
bytes: atlas.texture.data,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (samplerSub !== undefined) {
|
|
203
|
+
out.push({
|
|
204
|
+
guid: samplerSub.guid,
|
|
205
|
+
kind: 'sampler',
|
|
206
|
+
payload: makeAtlasSampler(),
|
|
207
|
+
refs: [],
|
|
208
|
+
artifacts: {},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (fontSub !== undefined) {
|
|
212
|
+
// The runtime font loader reads atlasGuid / samplerGuid / glyphs / common
|
|
213
|
+
// off the DDC payload (asset-registry loadFontAsset); the produced payload
|
|
214
|
+
// mirrors that shape. atlas Handle / sampler Handle are runtime-resolved
|
|
215
|
+
// from the GUID refs, so the build-time payload carries the GUID strings
|
|
216
|
+
// (cast through FontAsset for the ImportedAsset.payload Asset slot, same
|
|
217
|
+
// build-time POD-vs-Handle bridge the gltfImporter scene arm uses).
|
|
218
|
+
const fontPayload = {
|
|
219
|
+
kind: 'font',
|
|
220
|
+
atlasGuid: atlasSub?.guid ?? '',
|
|
221
|
+
samplerGuid: samplerSub?.guid ?? '',
|
|
222
|
+
glyphs: atlasGlyphsToMetrics(atlas),
|
|
223
|
+
common: makeFontCommon(atlas),
|
|
224
|
+
} as unknown as FontAsset;
|
|
225
|
+
out.push({
|
|
226
|
+
guid: fontSub.guid,
|
|
227
|
+
kind: 'font',
|
|
228
|
+
payload: fontPayload,
|
|
229
|
+
refs: [
|
|
230
|
+
...(atlasSub !== undefined ? [{ guid: atlasSub.guid }] : []),
|
|
231
|
+
...(samplerSub !== undefined ? [{ guid: samplerSub.guid }] : []),
|
|
232
|
+
],
|
|
233
|
+
artifacts: {},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return { ok: true, value: { assets: out, sourceDependencies: [] } };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The font {@link Importer}. Register it into an `ImporterRegistry` so the
|
|
241
|
+
* import runner dispatches `meta.importer === 'font'` sidecars here.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* import { ImporterRegistry } from '@forgeax/engine-import';
|
|
246
|
+
* import { fontImporter } from '@forgeax/engine-font/font-importer';
|
|
247
|
+
* const importers = new ImporterRegistry();
|
|
248
|
+
* importers.register(fontImporter);
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export const fontImporter: Importer = {
|
|
252
|
+
key: 'font',
|
|
253
|
+
import: importFont,
|
|
254
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @forgeax/engine-font - runtime-safe font asset contribution.
|
|
2
|
+
//
|
|
3
|
+
// Build-time baking lives behind the explicit Node-only `./cli-font` and
|
|
4
|
+
// `./font-importer` subpaths. Keeping the main entry free of the CLI's
|
|
5
|
+
// `node:buffer`/WASM graph prevents App bundles from pulling the producer
|
|
6
|
+
// toolchain into the browser runtime.
|
|
7
|
+
export { fontContribution } from './runtime/font-decoder.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
if (parentPort === null) {
|
|
4
|
+
throw new Error('the MSDF Node worker requires worker_threads.parentPort');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const listeners = new Map();
|
|
8
|
+
const endpoint = globalThis;
|
|
9
|
+
|
|
10
|
+
class NodeImageData {
|
|
11
|
+
constructor(data, width, height) {
|
|
12
|
+
this.data = data;
|
|
13
|
+
this.width = width;
|
|
14
|
+
this.height = height;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
endpoint.ImageData = NodeImageData;
|
|
19
|
+
endpoint.addEventListener = (type, listener) => {
|
|
20
|
+
if (type !== 'message') return;
|
|
21
|
+
const handler = (data) => listener({ data, origin: '*' });
|
|
22
|
+
listeners.set(listener, handler);
|
|
23
|
+
parentPort.on('message', handler);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
endpoint.removeEventListener = (type, listener) => {
|
|
27
|
+
if (type !== 'message') return;
|
|
28
|
+
const handler = listeners.get(listener);
|
|
29
|
+
if (handler === undefined) return;
|
|
30
|
+
listeners.delete(listener);
|
|
31
|
+
parentPort.off('message', handler);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
endpoint.postMessage = (message, transferList = []) => {
|
|
35
|
+
parentPort.postMessage(message, [...transferList]);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await import('@zappar/msdf-generator/worker.js');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
interface MessageEventLike {
|
|
4
|
+
readonly data: unknown;
|
|
5
|
+
readonly origin: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type MessageListener = (event: MessageEventLike) => void;
|
|
9
|
+
|
|
10
|
+
/** Comlink's browser Worker-shaped endpoint backed by a Node worker thread. */
|
|
11
|
+
export class NodeWorkerAdapter {
|
|
12
|
+
private readonly worker: Worker;
|
|
13
|
+
private readonly listeners = new Map<MessageListener, (data: unknown) => void>();
|
|
14
|
+
|
|
15
|
+
public constructor(url: string | URL) {
|
|
16
|
+
this.worker = new Worker(url);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public postMessage(message: unknown, transferList: readonly ArrayBuffer[] = []): void {
|
|
20
|
+
this.worker.postMessage(message, [...transferList]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public addEventListener(type: string, listener: MessageListener): void {
|
|
24
|
+
if (type !== 'message') return;
|
|
25
|
+
const handler = (data: unknown) => listener({ data, origin: '*' });
|
|
26
|
+
this.listeners.set(listener, handler);
|
|
27
|
+
this.worker.on('message', handler);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public removeEventListener(type: string, listener: MessageListener): void {
|
|
31
|
+
if (type !== 'message') return;
|
|
32
|
+
const handler = this.listeners.get(listener);
|
|
33
|
+
if (handler === undefined) return;
|
|
34
|
+
this.listeners.delete(listener);
|
|
35
|
+
this.worker.off('message', handler);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public terminate(): Promise<number> {
|
|
39
|
+
return this.worker.terminate();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
2
|
+
import {
|
|
3
|
+
type AssetDecoderContribution,
|
|
4
|
+
type AssetKind,
|
|
5
|
+
err,
|
|
6
|
+
type FontAsset,
|
|
7
|
+
ok,
|
|
8
|
+
} from '@forgeax/engine-types';
|
|
9
|
+
|
|
10
|
+
function parseGuid(value: unknown): FontAsset['atlas'] | undefined {
|
|
11
|
+
if (value instanceof Uint8Array && value.length === 16) return value as FontAsset['atlas'];
|
|
12
|
+
if (
|
|
13
|
+
Array.isArray(value) &&
|
|
14
|
+
value.length === 16 &&
|
|
15
|
+
value.every((item) => Number.isInteger(item) && item >= 0 && item <= 255)
|
|
16
|
+
) {
|
|
17
|
+
return Uint8Array.from(value) as FontAsset['atlas'];
|
|
18
|
+
}
|
|
19
|
+
if (typeof value !== 'string') return undefined;
|
|
20
|
+
const parsed = AssetGuid.parse(value);
|
|
21
|
+
return parsed.ok ? parsed.value : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validCommon(value: unknown): value is FontAsset['common'] {
|
|
25
|
+
if (value === null || typeof value !== 'object') return false;
|
|
26
|
+
const common = value as Record<string, unknown>;
|
|
27
|
+
return ['lineHeight', 'base', 'distanceRange', 'pxRange', 'atlasWidth', 'atlasHeight'].every(
|
|
28
|
+
(key) => typeof common[key] === 'number' && Number.isFinite(common[key]),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const fontContribution: AssetDecoderContribution<FontAsset, 'font'> = {
|
|
33
|
+
kind: { kind: 'font' } as AssetKind<FontAsset, 'font'>,
|
|
34
|
+
consumer: 'GlyphTextLayout',
|
|
35
|
+
decoder: {
|
|
36
|
+
async decode({ envelope }) {
|
|
37
|
+
const payload = envelope.payload as unknown;
|
|
38
|
+
if (payload !== null && typeof payload === 'object') {
|
|
39
|
+
const source = payload as Record<string, unknown>;
|
|
40
|
+
const atlas = parseGuid(source.atlas ?? source.atlasGuid);
|
|
41
|
+
const sampler = parseGuid(source.sampler ?? source.samplerGuid);
|
|
42
|
+
if (
|
|
43
|
+
(source.kind === undefined || source.kind === 'font') &&
|
|
44
|
+
atlas !== undefined &&
|
|
45
|
+
sampler !== undefined &&
|
|
46
|
+
source.glyphs !== null &&
|
|
47
|
+
typeof source.glyphs === 'object' &&
|
|
48
|
+
validCommon(source.common)
|
|
49
|
+
) {
|
|
50
|
+
return ok({
|
|
51
|
+
kind: 'font',
|
|
52
|
+
atlas,
|
|
53
|
+
sampler,
|
|
54
|
+
glyphs: source.glyphs as FontAsset['glyphs'],
|
|
55
|
+
common: source.common,
|
|
56
|
+
...(source.notdef === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { notdef: source.notdef as NonNullable<FontAsset['notdef']> }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return err({
|
|
63
|
+
code: 'asset-package-invalid',
|
|
64
|
+
expected: 'a font payload with atlas and sampler references',
|
|
65
|
+
hint: 'recook the font atlas and publish its local sub-assets',
|
|
66
|
+
detail: { guid: envelope.guid, reason: 'font owner validation failed' },
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|