@geastack/core 0.1.39 → 0.1.40

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.
@@ -1,60 +1,87 @@
1
1
  #!/usr/bin/env node
2
- import fs from 'node:fs'
3
- import path from 'node:path'
4
- import process from 'node:process'
5
- import zlib from 'node:zlib'
6
- import { execFileSync } from 'node:child_process'
7
- import { createHash } from 'node:crypto'
8
- import { createRequire } from 'node:module'
9
- import { fileURLToPath } from 'node:url'
10
-
11
- const args = process.argv.slice(2)
2
+ import fs from "node:fs";
3
+ import {
4
+ buildBitmapFont,
5
+ loadBitmapFontSource,
6
+ } from "./bitmap-font-source.mjs";
7
+ import path from "node:path";
8
+ import process from "node:process";
9
+ import zlib from "node:zlib";
10
+ import { execFileSync } from "node:child_process";
11
+ import { createHash } from "node:crypto";
12
+ import { createRequire } from "node:module";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const args = process.argv.slice(2);
12
16
 
13
17
  function readOption(name) {
14
- const index = args.indexOf(name)
15
- return index >= 0 ? args[index + 1] : undefined
18
+ const index = args.indexOf(name);
19
+ return index >= 0 ? args[index + 1] : undefined;
16
20
  }
17
21
 
18
22
  function hasFlag(name) {
19
- return args.includes(name)
23
+ return args.includes(name);
20
24
  }
21
25
 
22
26
  function readAllOptions(name) {
23
- const values = []
27
+ const values = [];
24
28
  for (let i = 0; i < args.length; i++) {
25
- if (args[i] === name && i + 1 < args.length) values.push(args[i + 1])
29
+ if (args[i] === name && i + 1 < args.length) values.push(args[i + 1]);
26
30
  }
27
- return values
31
+ return values;
28
32
  }
29
33
 
30
34
  function fail(message) {
31
- process.stderr.write(`${message}\n`)
32
- process.exit(1)
35
+ process.stderr.write(`${message}\n`);
36
+ process.exit(1);
33
37
  }
34
38
 
35
- const appDir = path.resolve(readOption('--app-dir') ?? fail('missing --app-dir <dir>'))
36
- const cssDir = path.resolve(readOption('--css-dir') ?? fail('missing --css-dir <dir>'))
37
- const outCpp = path.resolve(readOption('--out-cpp') ?? fail('missing --out-cpp <file>'))
38
- const outH = path.resolve(readOption('--out-h') ?? fail('missing --out-h <file>'))
39
- const symbolPrefix = readOption('--symbol-prefix')
40
- const viewportWidths = parseViewportWidths(readAllOptions('--viewport-width'))
41
- const viewportHeights = parseViewportWidths(readAllOptions('--viewport-height'))
42
- const devicePixelRatios = parseDevicePixelRatios(readAllOptions('--device-pixel-ratio'))
43
- const runtimeTtfFonts = hasFlag('--runtime-ttf-fonts')
39
+ const appDir = path.resolve(
40
+ readOption("--app-dir") ?? fail("missing --app-dir <dir>"),
41
+ );
42
+ const cssDir = path.resolve(
43
+ readOption("--css-dir") ?? fail("missing --css-dir <dir>"),
44
+ );
45
+ const outCpp = path.resolve(
46
+ readOption("--out-cpp") ?? fail("missing --out-cpp <file>"),
47
+ );
48
+ const outH = path.resolve(
49
+ readOption("--out-h") ?? fail("missing --out-h <file>"),
50
+ );
51
+ const symbolPrefix = readOption("--symbol-prefix");
52
+ const viewportWidths = parseViewportWidths(readAllOptions("--viewport-width"));
53
+ const viewportHeights = parseViewportWidths(
54
+ readAllOptions("--viewport-height"),
55
+ );
56
+ const devicePixelRatios = parseDevicePixelRatios(
57
+ readAllOptions("--device-pixel-ratio"),
58
+ );
59
+ const runtimeTtfFonts = hasFlag("--runtime-ttf-fonts");
44
60
  // Glyph rasterization supersampling. 4 (default) = antialiased coverage for
45
61
  // grayscale/color panels. 1 = sample at 1:1 = NO antialiasing = crisp binary
46
62
  // glyphs, which read better on 1-bit B/W e-paper than thresholded AA (every
47
63
  // glyph pixel is fully on/off, so the panel's mono threshold is a no-op).
48
64
  // Set per board by build-gea-vite-geatsc.mjs.
49
- const glyphSupersample = Math.max(1, Math.round(Number(readOption('--supersample') ?? '4')) || 4)
65
+ const glyphSupersample = Math.max(
66
+ 1,
67
+ Math.round(Number(readOption("--supersample") ?? "4")) || 4,
68
+ );
50
69
  // Atlas pixel depth. 8 (default) = one coverage byte per pixel. 2 = four pixels
51
70
  // per byte, quantized offline to the four levels a 4-gray e-paper panel can
52
71
  // actually show — the atlas costs a quarter of the flash bandwidth to read for
53
72
  // no visible loss on such a panel. Set per board by build-gea-vite-geatsc.mjs.
54
- const atlasBits = Number(readOption('--atlas-bits') ?? '8')
55
- if (atlasBits !== 2 && atlasBits !== 8) fail(`unsupported --atlas-bits ${atlasBits} (expected 2 or 8)`)
56
-
57
- const genericFamilies = new Set(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui'])
73
+ const atlasBits = Number(readOption("--atlas-bits") ?? "8");
74
+ if (atlasBits !== 2 && atlasBits !== 8)
75
+ fail(`unsupported --atlas-bits ${atlasBits} (expected 2 or 8)`);
76
+
77
+ const genericFamilies = new Set([
78
+ "serif",
79
+ "sans-serif",
80
+ "monospace",
81
+ "cursive",
82
+ "fantasy",
83
+ "system-ui",
84
+ ]);
58
85
 
59
86
  // Baseline glyph coverage for prose and UI chrome. EPUB text routinely uses
60
87
  // typographic quotes, dashes and ellipses even when the language is otherwise
@@ -93,13 +120,13 @@ const embeddedFontExtraCodepoints = [
93
120
  0x2192, // right arrow
94
121
  0x2193, // down arrow
95
122
  0x21bb, // clockwise open circle arrow
96
- ]
123
+ ];
97
124
 
98
125
  function baselineCodepoints() {
99
- const codepoints = []
100
- for (let cp = 0x20; cp <= 0x7e; cp += 1) codepoints.push(cp)
101
- codepoints.push(...embeddedFontExtraCodepoints)
102
- return codepoints
126
+ const codepoints = [];
127
+ for (let cp = 0x20; cp <= 0x7e; cp += 1) codepoints.push(cp);
128
+ codepoints.push(...embeddedFontExtraCodepoints);
129
+ return codepoints;
103
130
  }
104
131
 
105
132
  // Every atlas carries that whole baseline, at every size the app asks for. A
@@ -116,20 +143,20 @@ function baselineCodepoints() {
116
143
  // (`\2014` for an em dash) are honoured, so a stylesheet never has to carry a
117
144
  // literal non-ASCII character to name one.
118
145
  function parseFontCharset(value) {
119
- const text = unquoteCss(value.trim())
120
- if (text === '') return null
146
+ const text = unquoteCss(value.trim());
147
+ if (text === "") return null;
121
148
  // Space is a glyph like any other, and `?` is what the renderer substitutes
122
149
  // for one it cannot find, so neither is ever worth leaving out.
123
- const codepoints = new Set([0x20, 0x3f])
150
+ const codepoints = new Set([0x20, 0x3f]);
124
151
  // A CSS escape is a backslash, up to six hex digits, and one optional space
125
152
  // that ends the digits rather than being a character in its own right.
126
- const escapeRe = /\\([0-9a-fA-F]{1,6})[ ]?|\\(.)|([\s\S])/gu
127
- let escape
153
+ const escapeRe = /\\([0-9a-fA-F]{1,6})[ ]?|\\(.)|([\s\S])/gu;
154
+ let escape;
128
155
  while ((escape = escapeRe.exec(text)) !== null) {
129
- if (escape[1] !== undefined) codepoints.add(Number.parseInt(escape[1], 16))
130
- else codepoints.add((escape[2] ?? escape[3]).codePointAt(0))
156
+ if (escape[1] !== undefined) codepoints.add(Number.parseInt(escape[1], 16));
157
+ else codepoints.add((escape[2] ?? escape[3]).codePointAt(0));
131
158
  }
132
- return codepoints
159
+ return codepoints;
133
160
  }
134
161
 
135
162
  // null is "unrestricted", and it wins: a size is one atlas however many rules
@@ -137,120 +164,131 @@ function parseFontCharset(value) {
137
164
  // the atlas back to the baseline. Narrowing on the strength of the rules that
138
165
  // opted in would render the other rules' text as `?`.
139
166
  function mergeCharsets(a, b) {
140
- if (a === null || b === null) return null
141
- return new Set([...a, ...b])
167
+ if (a === null || b === null) return null;
168
+ return new Set([...a, ...b]);
142
169
  }
143
170
 
144
171
  function findCssFiles(dir) {
145
- const files = []
146
- const stack = [dir]
172
+ const files = [];
173
+ const stack = [dir];
147
174
  while (stack.length > 0) {
148
- const current = stack.pop()
149
- if (!current || !fs.existsSync(current)) continue
175
+ const current = stack.pop();
176
+ if (!current || !fs.existsSync(current)) continue;
150
177
  for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
151
- const full = path.join(current, entry.name)
152
- if (entry.isDirectory()) stack.push(full)
153
- else if (entry.isFile() && entry.name.endsWith('.css')) files.push(full)
178
+ const full = path.join(current, entry.name);
179
+ if (entry.isDirectory()) stack.push(full);
180
+ else if (entry.isFile() && entry.name.endsWith(".css")) files.push(full);
154
181
  }
155
182
  }
156
- files.sort()
157
- return files
183
+ files.sort();
184
+ return files;
158
185
  }
159
186
 
160
187
  function stripCssComments(css) {
161
- return css.replace(/\/\*[\s\S]*?\*\//g, '')
188
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
162
189
  }
163
190
 
164
191
  function unquoteCss(value) {
165
- const text = value.trim()
166
- if (text.length >= 2 && ((text[0] === '"' && text[text.length - 1] === '"') || (text[0] === "'" && text[text.length - 1] === "'"))) {
167
- return text.slice(1, -1)
192
+ const text = value.trim();
193
+ if (
194
+ text.length >= 2 &&
195
+ ((text[0] === '"' && text[text.length - 1] === '"') ||
196
+ (text[0] === "'" && text[text.length - 1] === "'"))
197
+ ) {
198
+ return text.slice(1, -1);
168
199
  }
169
- return text
200
+ return text;
170
201
  }
171
202
 
172
203
  function firstFontFamily(value) {
173
- let text = value.trim()
174
- if (!text) return null
204
+ let text = value.trim();
205
+ if (!text) return null;
175
206
  if (text[0] === '"' || text[0] === "'") {
176
- const quote = text[0]
177
- const end = text.indexOf(quote, 1)
178
- return end > 0 ? text.slice(1, end) : null
207
+ const quote = text[0];
208
+ const end = text.indexOf(quote, 1);
209
+ return end > 0 ? text.slice(1, end) : null;
179
210
  }
180
- const comma = text.indexOf(',')
181
- if (comma >= 0) text = text.slice(0, comma)
182
- const family = unquoteCss(text.trim())
183
- return family && !genericFamilies.has(family.toLowerCase()) ? family : null
211
+ const comma = text.indexOf(",");
212
+ if (comma >= 0) text = text.slice(0, comma);
213
+ const family = unquoteCss(text.trim());
214
+ return family && !genericFamilies.has(family.toLowerCase()) ? family : null;
184
215
  }
185
216
 
186
217
  function parseViewportWidths(values) {
187
- const widths = []
218
+ const widths = [];
188
219
  for (const value of values) {
189
- for (const part of String(value).split(',')) {
190
- const width = Math.round(Number(part.trim()))
191
- if (width > 0 && !widths.includes(width)) widths.push(width)
220
+ for (const part of String(value).split(",")) {
221
+ const width = Math.round(Number(part.trim()));
222
+ if (width > 0 && !widths.includes(width)) widths.push(width);
192
223
  }
193
224
  }
194
- return widths
225
+ return widths;
195
226
  }
196
227
 
197
228
  function parseDevicePixelRatios(values) {
198
- const ratios = []
229
+ const ratios = [];
199
230
  for (const value of values) {
200
- for (const part of String(value).split(',')) {
201
- const ratio = Number(part.trim())
202
- if (ratio > 0 && !ratios.includes(ratio)) ratios.push(ratio)
231
+ for (const part of String(value).split(",")) {
232
+ const ratio = Number(part.trim());
233
+ if (ratio > 0 && !ratios.includes(ratio)) ratios.push(ratio);
203
234
  }
204
235
  }
205
- return ratios.length > 0 ? ratios : [1]
236
+ return ratios.length > 0 ? ratios : [1];
206
237
  }
207
238
 
208
239
  function splitTopLevel(value, delimiter) {
209
- const out = []
210
- let start = 0
211
- let depth = 0
240
+ const out = [];
241
+ let start = 0;
242
+ let depth = 0;
212
243
  for (let i = 0; i < value.length; i++) {
213
- const char = value[i]
214
- if (char === '(') depth++
215
- else if (char === ')' && depth > 0) depth--
244
+ const char = value[i];
245
+ if (char === "(") depth++;
246
+ else if (char === ")" && depth > 0) depth--;
216
247
  else if (char === delimiter && depth === 0) {
217
- out.push(value.slice(start, i).trim())
218
- start = i + 1
248
+ out.push(value.slice(start, i).trim());
249
+ start = i + 1;
219
250
  }
220
251
  }
221
- out.push(value.slice(start).trim())
222
- return out.filter(Boolean)
252
+ out.push(value.slice(start).trim());
253
+ return out.filter(Boolean);
223
254
  }
224
255
 
225
256
  function functionInner(value, name) {
226
- const text = value.trim()
227
- const prefix = `${name}(`
228
- if (!text.toLowerCase().startsWith(prefix) || !text.endsWith(')')) return null
229
- return text.slice(prefix.length, -1)
257
+ const text = value.trim();
258
+ const prefix = `${name}(`;
259
+ if (!text.toLowerCase().startsWith(prefix) || !text.endsWith(")"))
260
+ return null;
261
+ return text.slice(prefix.length, -1);
230
262
  }
231
263
 
232
264
  function uniqueSizes(sizes) {
233
265
  return sizes
234
266
  .map((size) => Math.round(size))
235
- .filter((size, index, list) => size > 0 && list.indexOf(size) === index)
267
+ .filter((size, index, list) => size > 0 && list.indexOf(size) === index);
236
268
  }
237
269
 
238
270
  function viewportCombos() {
239
- if (viewportWidths.length === 0 && viewportHeights.length === 0) return [{ width: 0, height: 0 }]
240
- if (viewportWidths.length === 0) return viewportHeights.map((height) => ({ width: 0, height }))
241
- if (viewportHeights.length === 0) return viewportWidths.map((width) => ({ width, height: 0 }))
271
+ if (viewportWidths.length === 0 && viewportHeights.length === 0)
272
+ return [{ width: 0, height: 0 }];
273
+ if (viewportWidths.length === 0)
274
+ return viewportHeights.map((height) => ({ width: 0, height }));
275
+ if (viewportHeights.length === 0)
276
+ return viewportWidths.map((width) => ({ width, height: 0 }));
242
277
  if (viewportWidths.length === viewportHeights.length) {
243
- return viewportWidths.map((width, index) => ({ width, height: viewportHeights[index] }))
278
+ return viewportWidths.map((width, index) => ({
279
+ width,
280
+ height: viewportHeights[index],
281
+ }));
244
282
  }
245
- const combos = []
283
+ const combos = [];
246
284
  for (const width of viewportWidths) {
247
- for (const height of viewportHeights) combos.push({ width, height })
285
+ for (const height of viewportHeights) combos.push({ width, height });
248
286
  }
249
- return combos
287
+ return combos;
250
288
  }
251
289
 
252
290
  function defaultFontSizes() {
253
- return uniqueSizes(devicePixelRatios.map((dpr) => 16 * dpr))
291
+ return uniqueSizes(devicePixelRatios.map((dpr) => 16 * dpr));
254
292
  }
255
293
 
256
294
  // Resolve a single CSS length token to ONE physical-pixel size for a specific
@@ -267,42 +305,50 @@ function defaultFontSizes() {
267
305
  // which is also an exact atlas hit (the board's physical viewport divided by DPR
268
306
  // equals its logical viewport, so vh-no-DPR here == logical-vh x DPR at runtime).
269
307
  function resolveLengthForCombo(value, width, height, dpr) {
270
- const text = value.trim()
271
- const absoluteMatch = text.match(/^(\d+(?:\.\d+)?)(px)?$/)
308
+ const text = value.trim();
309
+ const absoluteMatch = text.match(/^(\d+(?:\.\d+)?)(px)?$/);
272
310
  if (absoluteMatch) {
273
- const cssPx = Number(absoluteMatch[1])
274
- return absoluteMatch[2] ? cssPx * dpr : cssPx
311
+ const cssPx = Number(absoluteMatch[1]);
312
+ return absoluteMatch[2] ? cssPx * dpr : cssPx;
275
313
  }
276
- const vwMatch = text.match(/^(\d+(?:\.\d+)?)vw$/)
277
- if (vwMatch) return (Number(vwMatch[1]) * width) / 100
278
- const vhMatch = text.match(/^(\d+(?:\.\d+)?)vh$/)
279
- if (vhMatch) return (Number(vhMatch[1]) * height) / 100
280
- const vminMatch = text.match(/^(\d+(?:\.\d+)?)vmin$/)
281
- if (vminMatch) return (Number(vminMatch[1]) * Math.min(width, height)) / 100
282
- const vmaxMatch = text.match(/^(\d+(?:\.\d+)?)vmax$/)
283
- if (vmaxMatch) return (Number(vmaxMatch[1]) * Math.max(width, height)) / 100
284
- const clampInner = functionInner(text, 'clamp')
314
+ const vwMatch = text.match(/^(\d+(?:\.\d+)?)vw$/);
315
+ if (vwMatch) return (Number(vwMatch[1]) * width) / 100;
316
+ const vhMatch = text.match(/^(\d+(?:\.\d+)?)vh$/);
317
+ if (vhMatch) return (Number(vhMatch[1]) * height) / 100;
318
+ const vminMatch = text.match(/^(\d+(?:\.\d+)?)vmin$/);
319
+ if (vminMatch) return (Number(vminMatch[1]) * Math.min(width, height)) / 100;
320
+ const vmaxMatch = text.match(/^(\d+(?:\.\d+)?)vmax$/);
321
+ if (vmaxMatch) return (Number(vmaxMatch[1]) * Math.max(width, height)) / 100;
322
+ const clampInner = functionInner(text, "clamp");
285
323
  if (clampInner !== null) {
286
- const parts = splitTopLevel(clampInner, ',')
287
- if (parts.length !== 3) return null
288
- const resolved = parts.map((part) => resolveLengthForCombo(part, width, height, dpr))
289
- if (resolved.some((size) => size === null)) return null
290
- const [lo, preferred, hi] = resolved
291
- return Math.max(lo, Math.min(preferred, hi))
324
+ const parts = splitTopLevel(clampInner, ",");
325
+ if (parts.length !== 3) return null;
326
+ const resolved = parts.map((part) =>
327
+ resolveLengthForCombo(part, width, height, dpr),
328
+ );
329
+ if (resolved.some((size) => size === null)) return null;
330
+ const [lo, preferred, hi] = resolved;
331
+ return Math.max(lo, Math.min(preferred, hi));
292
332
  }
293
- const minInner = functionInner(text, 'min')
333
+ const minInner = functionInner(text, "min");
294
334
  if (minInner !== null) {
295
- const resolved = splitTopLevel(minInner, ',').map((part) => resolveLengthForCombo(part, width, height, dpr))
296
- if (resolved.length === 0 || resolved.some((size) => size === null)) return null
297
- return Math.min(...resolved)
335
+ const resolved = splitTopLevel(minInner, ",").map((part) =>
336
+ resolveLengthForCombo(part, width, height, dpr),
337
+ );
338
+ if (resolved.length === 0 || resolved.some((size) => size === null))
339
+ return null;
340
+ return Math.min(...resolved);
298
341
  }
299
- const maxInner = functionInner(text, 'max')
342
+ const maxInner = functionInner(text, "max");
300
343
  if (maxInner !== null) {
301
- const resolved = splitTopLevel(maxInner, ',').map((part) => resolveLengthForCombo(part, width, height, dpr))
302
- if (resolved.length === 0 || resolved.some((size) => size === null)) return null
303
- return Math.max(...resolved)
344
+ const resolved = splitTopLevel(maxInner, ",").map((part) =>
345
+ resolveLengthForCombo(part, width, height, dpr),
346
+ );
347
+ if (resolved.length === 0 || resolved.some((size) => size === null))
348
+ return null;
349
+ return Math.max(...resolved);
304
350
  }
305
- return null
351
+ return null;
306
352
  }
307
353
 
308
354
  // Bake one atlas size per distinct resolved value across every configured
@@ -311,128 +357,200 @@ function resolveLengthForCombo(value, width, height, dpr) {
311
357
  // uniqueSizes drops non-positive (viewport-relative tokens with no viewport) and
312
358
  // duplicate sizes. This mirrors the runtime so each requested px hits an atlas.
313
359
  function parseFontSizes(value) {
314
- const combos = viewportCombos()
315
- const sizes = []
360
+ const combos = viewportCombos();
361
+ const sizes = [];
316
362
  for (const dpr of devicePixelRatios) {
317
363
  for (const { width, height } of combos) {
318
- const size = resolveLengthForCombo(value, width, height, dpr)
319
- if (size !== null) sizes.push(size)
364
+ const size = resolveLengthForCombo(value, width, height, dpr);
365
+ if (size !== null) sizes.push(size);
320
366
  }
321
367
  }
322
- return uniqueSizes(sizes)
368
+ return uniqueSizes(sizes);
323
369
  }
324
370
 
325
- function simpleCssSelectors(selectorText) {
371
+ function parseFontShorthand(value) {
372
+ // Style/weight precede the required size; optional line-height follows it.
373
+ // Keep the family tail intact so quoted names and fallback lists use the
374
+ // same parser as an explicit font-family declaration.
375
+ const match = value.match(
376
+ /(?:^|\s)([+]?(?:\d*\.)?\d+(?:px|pt|em|rem|vw|vh|vmin|vmax|%))(?:\s*\/\s*[^\s]+)?\s+(.+)$/i,
377
+ );
378
+ if (!match) return null;
379
+ return {
380
+ family: firstFontFamily(match[2]),
381
+ sizePxs: parseFontSizes(match[1]),
382
+ };
383
+ }
384
+
385
+ function fontCssSelectors(selectorText) {
326
386
  return selectorText
327
- .split(',')
387
+ .split(",")
328
388
  .map((selector) => selector.trim())
329
- .filter((selector) => /^\.?[A-Za-z][A-Za-z0-9_-]*$/.test(selector))
389
+ .filter((selector) => selector.length > 0 && !selector.startsWith("@"));
330
390
  }
331
391
 
332
392
  function rememberFontTuple(tuples, fontFaces, family, sizePx, charset) {
333
- if (!family || !fontFaces.has(family)) return
334
- const resolvedSize = sizePx ?? defaultFontSizes()[0] ?? 16
335
- const key = `${family}\0${resolvedSize}`
336
- const existing = tuples.get(key)
337
- const merged = existing ? mergeCharsets(existing.charset, charset ?? null) : (charset ?? null)
338
- tuples.set(key, { family, sizePx: resolvedSize, charset: merged })
393
+ if (!family || !fontFaces.has(family)) return;
394
+ const resolvedSize = sizePx ?? defaultFontSizes()[0] ?? 16;
395
+ const key = `${family}\0${resolvedSize}`;
396
+ const existing = tuples.get(key);
397
+ const merged = existing
398
+ ? mergeCharsets(existing.charset, charset ?? null)
399
+ : (charset ?? null);
400
+ tuples.set(key, { family, sizePx: resolvedSize, charset: merged });
339
401
  }
340
402
 
341
403
  function rememberFontTuples(tuples, fontFaces, family, sizePxs, charset) {
342
404
  if (!sizePxs || sizePxs.length === 0) {
343
- for (const sizePx of defaultFontSizes()) rememberFontTuple(tuples, fontFaces, family, sizePx, null)
344
- return
405
+ for (const sizePx of defaultFontSizes())
406
+ rememberFontTuple(tuples, fontFaces, family, sizePx, null);
407
+ return;
345
408
  }
346
- for (const sizePx of sizePxs) rememberFontTuple(tuples, fontFaces, family, sizePx, charset)
409
+ for (const sizePx of sizePxs)
410
+ rememberFontTuple(tuples, fontFaces, family, sizePx, charset);
347
411
  }
348
412
 
349
413
  function parseFontFaces(css, cssFile) {
350
- const faces = []
351
- const cssDirName = path.dirname(cssFile)
352
- const re = /@font-face\s*\{([^}]+)\}/gi
353
- let match
414
+ const faces = [];
415
+ const cssDirName = path.dirname(cssFile);
416
+ const re = /@font-face\s*\{([^}]+)\}/gi;
417
+ let match;
354
418
  while ((match = re.exec(css)) !== null) {
355
- const block = match[1]
356
- const familyMatch = block.match(/font-family\s*:\s*(?:"([^"]+)"|'([^']+)'|([^;]+))/i)
357
- const srcMatch = block.match(/src\s*:\s*url\(\s*(?:"([^"]+)"|'([^']+)'|([^)"']+))\s*\)/i)
358
- const family = familyMatch ? unquoteCss((familyMatch[1] ?? familyMatch[2] ?? familyMatch[3]).trim()) : null
359
- const rawSrc = srcMatch ? (srcMatch[1] ?? srcMatch[2] ?? srcMatch[3]).trim() : null
360
- if (!family || !rawSrc || /^https?:\/\//i.test(rawSrc) || rawSrc.startsWith('data:')) continue
361
- const cleanSrc = rawSrc.split(/[?#]/, 1)[0]
362
- const resolved = path.resolve(cssDirName, cleanSrc)
363
- if (fs.existsSync(resolved)) faces.push({ family, src: resolved })
419
+ const block = match[1];
420
+ const familyMatch = block.match(
421
+ /font-family\s*:\s*(?:"([^"]+)"|'([^']+)'|([^;]+))/i,
422
+ );
423
+ const srcMatch = block.match(
424
+ /src\s*:\s*url\(\s*(?:"([^"]+)"|'([^']+)'|([^)"']+))\s*\)/i,
425
+ );
426
+ const family = familyMatch
427
+ ? unquoteCss((familyMatch[1] ?? familyMatch[2] ?? familyMatch[3]).trim())
428
+ : null;
429
+ const rawSrc = srcMatch
430
+ ? (srcMatch[1] ?? srcMatch[2] ?? srcMatch[3]).trim()
431
+ : null;
432
+ if (
433
+ !family ||
434
+ !rawSrc ||
435
+ /^https?:\/\//i.test(rawSrc) ||
436
+ rawSrc.startsWith("data:")
437
+ )
438
+ continue;
439
+ const cleanSrc = rawSrc.split(/[?#]/, 1)[0];
440
+ const resolved = path.resolve(cssDirName, cleanSrc);
441
+ if (fs.existsSync(resolved)) {
442
+ const embeddedMatch = block.match(
443
+ /--gea-font-source\s*:\s*url\(\s*(?:"([^"]+)"|'([^']+)'|([^)"']+))\s*\)/i,
444
+ );
445
+ const embedded = embeddedMatch
446
+ ? path.resolve(
447
+ cssDirName,
448
+ (embeddedMatch[1] ?? embeddedMatch[2] ?? embeddedMatch[3]).trim(),
449
+ )
450
+ : null;
451
+ if (embedded && !fs.existsSync(embedded))
452
+ fail(`missing bitmap font source: ${embedded}`);
453
+ faces.push({ family, src: resolved, embedded });
454
+ }
364
455
  }
365
- return faces
456
+ return faces;
366
457
  }
367
458
 
368
459
  function collectSourceFontFaces() {
369
460
  // The app, then whatever the project shares between its apps. The project is
370
461
  // the directory this ran in.
371
- const sourceDirs = [appDir, path.join(process.cwd(), 'components')]
372
- const fontFaces = new Map()
462
+ const sourceDirs = [appDir, path.join(process.cwd(), "components")];
463
+ const fontFaces = new Map();
464
+ fontFaces.embeddedSources = new Map();
373
465
  for (const dir of sourceDirs) {
374
466
  for (const file of findCssFiles(dir)) {
375
- const css = stripCssComments(fs.readFileSync(file, 'utf8'))
467
+ const css = stripCssComments(fs.readFileSync(file, "utf8"));
376
468
  for (const face of parseFontFaces(css, file)) {
377
- if (!fontFaces.has(face.family)) fontFaces.set(face.family, face.src)
469
+ if (!fontFaces.has(face.family)) {
470
+ fontFaces.set(face.family, face.src);
471
+ if (face.embedded)
472
+ fontFaces.embeddedSources.set(face.family, face.embedded);
473
+ }
378
474
  }
379
475
  }
380
476
  }
381
- return fontFaces
477
+ return fontFaces;
382
478
  }
383
479
 
384
480
  function collectUsedFontTuples(fontFaces) {
385
- const tuples = new Map()
386
- const usedFamilies = new Set()
387
- const inheritedSizes = new Map()
481
+ const tuples = new Map();
482
+ const usedFamilies = new Set();
483
+ const inheritedSizes = new Map();
388
484
  for (const file of findCssFiles(cssDir)) {
389
- const css = stripCssComments(fs.readFileSync(file, 'utf8'))
390
- const selectorStates = new Map()
391
- const ruleRe = /([^{}]+)\{([^{}]*)\}/g
392
- let match
485
+ const css = stripCssComments(fs.readFileSync(file, "utf8"));
486
+ const selectorStates = new Map();
487
+ const ruleRe = /([^{}]+)\{([^{}]*)\}/g;
488
+ let match;
393
489
  while ((match = ruleRe.exec(css)) !== null) {
394
- const selector = match[1].trim()
395
- if (selector.startsWith('@')) continue
396
- const selectors = simpleCssSelectors(selector)
397
- if (selectors.length === 0) continue
398
- let family = null
399
- let sizePxs = null
400
- let charset = null
401
- for (const declaration of match[2].split(';')) {
402
- const colon = declaration.indexOf(':')
403
- if (colon <= 0) continue
404
- const name = declaration.slice(0, colon).trim().toLowerCase()
405
- const value = declaration.slice(colon + 1).trim()
406
- if (name === 'font-family') family = firstFontFamily(value)
407
- else if (name === 'font-size') sizePxs = parseFontSizes(value)
408
- else if (name === '--gea-font-charset') charset = parseFontCharset(value)
490
+ const selector = match[1].trim();
491
+ if (selector.startsWith("@")) continue;
492
+ const selectors = fontCssSelectors(selector);
493
+ if (selectors.length === 0) continue;
494
+ let family = null;
495
+ let sizePxs = null;
496
+ let charset = null;
497
+ for (const declaration of match[2].split(";")) {
498
+ const colon = declaration.indexOf(":");
499
+ if (colon <= 0) continue;
500
+ const name = declaration.slice(0, colon).trim().toLowerCase();
501
+ const value = declaration.slice(colon + 1).trim();
502
+ if (name === "font-family") family = firstFontFamily(value);
503
+ else if (name === "font-size") sizePxs = parseFontSizes(value);
504
+ else if (name === "font") {
505
+ const shorthand = parseFontShorthand(value);
506
+ if (shorthand) {
507
+ family = shorthand.family;
508
+ sizePxs = shorthand.sizePxs;
509
+ }
510
+ } else if (name === "--gea-font-charset")
511
+ charset = parseFontCharset(value);
409
512
  }
410
513
  // Accumulate every family/size a selector is ever assigned: repeated
411
514
  // rules are usually media-query branches (the block regex sees through
412
515
  // `@media { ... }` wrappers), and the generator can't evaluate media
413
516
  // conditions, so every branch needs an atlas.
414
517
  for (const selector of selectors) {
415
- const state = selectorStates.get(selector) ?? { families: new Set(), sizePxs: new Set(), charset: undefined }
416
- if (family) state.families.add(family)
518
+ const state = selectorStates.get(selector) ?? {
519
+ families: new Set(),
520
+ sizePxs: new Set(),
521
+ charset: undefined,
522
+ };
523
+ if (family) state.families.add(family);
417
524
  // Only a rule that names a size has an opinion about that size's atlas.
418
525
  // A rule on the same selector that just sets a colour must not widen it.
419
526
  if (sizePxs) {
420
- for (const sizePx of sizePxs) state.sizePxs.add(sizePx)
421
- state.charset = state.charset === undefined ? charset : mergeCharsets(state.charset, charset)
527
+ for (const sizePx of sizePxs) state.sizePxs.add(sizePx);
528
+ state.charset =
529
+ state.charset === undefined
530
+ ? charset
531
+ : mergeCharsets(state.charset, charset);
422
532
  }
423
- selectorStates.set(selector, state)
533
+ selectorStates.set(selector, state);
424
534
  }
425
535
  }
426
536
  for (const state of selectorStates.values()) {
427
- const charset = state.charset ?? null
537
+ const charset = state.charset ?? null;
428
538
  for (const family of state.families) {
429
- if (fontFaces.has(family)) usedFamilies.add(family)
430
- rememberFontTuples(tuples, fontFaces, family, [...state.sizePxs], charset)
539
+ if (fontFaces.has(family)) usedFamilies.add(family);
540
+ rememberFontTuples(
541
+ tuples,
542
+ fontFaces,
543
+ family,
544
+ [...state.sizePxs],
545
+ charset,
546
+ );
431
547
  }
432
548
  if (state.families.size === 0) {
433
549
  for (const sizePx of state.sizePxs) {
434
- const merged = inheritedSizes.has(sizePx) ? mergeCharsets(inheritedSizes.get(sizePx), charset) : charset
435
- inheritedSizes.set(sizePx, merged)
550
+ const merged = inheritedSizes.has(sizePx)
551
+ ? mergeCharsets(inheritedSizes.get(sizePx), charset)
552
+ : charset;
553
+ inheritedSizes.set(sizePx, merged);
436
554
  }
437
555
  }
438
556
  }
@@ -448,65 +566,100 @@ function collectUsedFontTuples(fontFaces) {
448
566
  if (inheritedSizes.size > 0) {
449
567
  for (const family of usedFamilies) {
450
568
  for (const [sizePx, charset] of inheritedSizes) {
451
- rememberFontTuples(tuples, fontFaces, family, [sizePx], charset)
569
+ rememberFontTuples(tuples, fontFaces, family, [sizePx], charset);
452
570
  }
453
571
  }
454
572
  }
455
- return [...tuples.values()].sort((a, b) => a.family.localeCompare(b.family) || a.sizePx - b.sizePx)
573
+ return [...tuples.values()].sort(
574
+ (a, b) => a.family.localeCompare(b.family) || a.sizePx - b.sizePx,
575
+ );
456
576
  }
457
577
 
458
578
  function loadOpentype() {
459
579
  const packageJsons = [
460
- path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'),
461
- path.join(appDir, 'package.json'),
462
- path.join(process.cwd(), 'package.json'),
463
- ]
580
+ path.join(
581
+ path.dirname(fileURLToPath(import.meta.url)),
582
+ "..",
583
+ "package.json",
584
+ ),
585
+ path.join(appDir, "package.json"),
586
+ path.join(process.cwd(), "package.json"),
587
+ ];
464
588
  for (const packageJson of packageJsons) {
465
- if (!fs.existsSync(packageJson)) continue
589
+ if (!fs.existsSync(packageJson)) continue;
466
590
  try {
467
- return createRequire(packageJson)('opentype.js')
591
+ return createRequire(packageJson)("opentype.js");
468
592
  } catch {
469
593
  // Try the next package root.
470
594
  }
471
595
  }
472
- fail('missing opentype.js from the @geastack/core installation')
596
+ fail("missing opentype.js from the @geastack/core installation");
473
597
  }
474
598
 
475
- function rasterizeFont(opentype, fontPath, family, familyId, sizePx, fontId, charset) {
476
- const fontBuffer = fs.readFileSync(fontPath)
477
- const arrayBuffer = fontBuffer.buffer.slice(fontBuffer.byteOffset, fontBuffer.byteOffset + fontBuffer.byteLength)
478
- const font = opentype.parse(arrayBuffer)
479
-
480
- const scale = sizePx / font.unitsPerEm
481
- const ascender = Math.ceil(font.ascender * scale)
482
- const descender = Math.ceil(Math.abs(font.descender * scale))
483
- const lineHeight = ascender + descender
484
- const glyphs = []
485
- const glyphBitmaps = []
486
-
487
- const codepoints = charset ? [...charset].sort((a, b) => a - b) : baselineCodepoints()
599
+ function rasterizeFont(
600
+ opentype,
601
+ fontPath,
602
+ family,
603
+ familyId,
604
+ sizePx,
605
+ fontId,
606
+ charset,
607
+ ) {
608
+ const fontBuffer = fs.readFileSync(fontPath);
609
+ const arrayBuffer = fontBuffer.buffer.slice(
610
+ fontBuffer.byteOffset,
611
+ fontBuffer.byteOffset + fontBuffer.byteLength,
612
+ );
613
+ const font = opentype.parse(arrayBuffer);
614
+
615
+ const scale = sizePx / font.unitsPerEm;
616
+ const ascender = Math.ceil(font.ascender * scale);
617
+ const descender = Math.ceil(Math.abs(font.descender * scale));
618
+ const lineHeight = ascender + descender;
619
+ const glyphs = [];
620
+ const glyphBitmaps = [];
621
+
622
+ const codepoints = charset
623
+ ? [...charset].sort((a, b) => a - b)
624
+ : baselineCodepoints();
488
625
 
489
626
  for (const cp of codepoints) {
490
- const glyph = font.charToGlyph(String.fromCodePoint(cp))
491
- const advance = Math.round((glyph.advanceWidth ?? 0) * scale)
492
- const bounds = glyph.getBoundingBox()
493
- const x0 = Math.floor(bounds.x1 * scale)
494
- const y0 = Math.floor(-bounds.y2 * scale)
495
- const x1 = Math.ceil(bounds.x2 * scale)
496
- const y1 = Math.ceil(-bounds.y1 * scale)
497
- const gw = Math.max(x1 - x0, 0)
498
- const gh = Math.max(y1 - y0, 0)
499
- const bitmap = new Uint8Array(gw * gh)
627
+ const glyph = font.charToGlyph(String.fromCodePoint(cp));
628
+ const advance = Math.round((glyph.advanceWidth ?? 0) * scale);
629
+ const bounds = glyph.getBoundingBox();
630
+ const x0 = Math.floor(bounds.x1 * scale);
631
+ const y0 = Math.floor(-bounds.y2 * scale);
632
+ const x1 = Math.ceil(bounds.x2 * scale);
633
+ const y1 = Math.ceil(-bounds.y1 * scale);
634
+ const gw = Math.max(x1 - x0, 0);
635
+ const gh = Math.max(y1 - y0, 0);
636
+ const bitmap = new Uint8Array(gw * gh);
500
637
 
501
638
  if (gw > 0 && gh > 0) {
502
- rasterizeGlyph(glyph.getPath(0, 0, sizePx).commands, bitmap, gw, gh, x0, y0)
639
+ rasterizeGlyph(
640
+ glyph.getPath(0, 0, sizePx).commands,
641
+ bitmap,
642
+ gw,
643
+ gh,
644
+ x0,
645
+ y0,
646
+ );
503
647
  }
504
648
 
505
- glyphs.push({ codepoint: cp, sourceX: 0, sourceY: 0, width: gw, height: gh, advance, bearingX: x0, bearingY: -y0 })
506
- glyphBitmaps.push({ data: bitmap, width: gw, height: gh })
649
+ glyphs.push({
650
+ codepoint: cp,
651
+ sourceX: 0,
652
+ sourceY: 0,
653
+ width: gw,
654
+ height: gh,
655
+ advance,
656
+ bearingX: x0,
657
+ bearingY: -y0,
658
+ });
659
+ glyphBitmaps.push({ data: bitmap, width: gw, height: gh });
507
660
  }
508
661
 
509
- const { atlasWidth, atlasHeight } = packGlyphAtlas(glyphs)
662
+ const { atlasWidth, atlasHeight } = packGlyphAtlas(glyphs);
510
663
  if (atlasWidth === 0 || atlasHeight === 0) {
511
664
  return {
512
665
  id: fontId,
@@ -521,19 +674,23 @@ function rasterizeFont(opentype, fontPath, family, familyId, sizePx, fontId, cha
521
674
  atlasHeight: 1,
522
675
  atlasBits,
523
676
  atlasData: new Uint8Array(1),
524
- }
677
+ };
525
678
  }
526
679
 
527
- const atlasData = new Uint8Array(atlasWidth * atlasHeight)
680
+ const atlasData = new Uint8Array(atlasWidth * atlasHeight);
528
681
  for (let i = 0; i < glyphs.length; i += 1) {
529
- const glyph = glyphs[i]
530
- const bitmap = glyphBitmaps[i]
682
+ const glyph = glyphs[i];
683
+ const bitmap = glyphBitmaps[i];
531
684
  // Quantize per glyph, not across the packed atlas: the neighbourhood rules
532
685
  // below must not see a neighbouring glyph's ink through the 1px padding.
533
- const source = atlasBits === 2 ? quantizeCoverageToLevels(bitmap.data, bitmap.width, bitmap.height) : bitmap.data
686
+ const source =
687
+ atlasBits === 2
688
+ ? quantizeCoverageToLevels(bitmap.data, bitmap.width, bitmap.height)
689
+ : bitmap.data;
534
690
  for (let row = 0; row < bitmap.height; row += 1) {
535
691
  for (let col = 0; col < bitmap.width; col += 1) {
536
- atlasData[(glyph.sourceY + row) * atlasWidth + glyph.sourceX + col] = source[row * bitmap.width + col]
692
+ atlasData[(glyph.sourceY + row) * atlasWidth + glyph.sourceX + col] =
693
+ source[row * bitmap.width + col];
537
694
  }
538
695
  }
539
696
  }
@@ -550,8 +707,11 @@ function rasterizeFont(opentype, fontPath, family, familyId, sizePx, fontId, cha
550
707
  atlasWidth,
551
708
  atlasHeight,
552
709
  atlasBits,
553
- atlasData: atlasBits === 2 ? packLevelsTo2Bit(atlasData, atlasWidth, atlasHeight) : atlasData,
554
- }
710
+ atlasData:
711
+ atlasBits === 2
712
+ ? packLevelsTo2Bit(atlasData, atlasWidth, atlasHeight)
713
+ : atlasData,
714
+ };
555
715
  }
556
716
 
557
717
  // Offline quantization of 8-bit supersampled coverage to the four levels of a
@@ -580,20 +740,22 @@ function rasterizeFont(opentype, fontPath, family, familyId, sizePx, fontId, cha
580
740
  // - The 48 floor drops coverage below ~19% instead of promoting it to level 1
581
741
  // (85/255 = 33%), so faint spill outside a stroke does not fatten it.
582
742
  function quantizeCoverageToLevels(coverage, width, height) {
583
- const levels = new Uint8Array(width * height)
743
+ const levels = new Uint8Array(width * height);
584
744
  for (let i = 0; i < coverage.length; i += 1) {
585
- const value = coverage[i]
586
- levels[i] = value >= 208 ? 3 : value >= 128 ? 2 : value >= 48 ? 1 : 0
745
+ const value = coverage[i];
746
+ levels[i] = value >= 208 ? 3 : value >= 128 ? 2 : value >= 48 ? 1 : 0;
587
747
  }
588
748
 
589
749
  // Neighbourhood fixups, read from the pass above and written to a copy so the
590
750
  // result does not depend on scan order.
591
- const result = Uint8Array.from(levels)
592
- const levelAt = (x, y) => (x < 0 || y < 0 || x >= width || y >= height ? 0 : levels[y * width + x])
593
- const coverageAt = (x, y) => (x < 0 || y < 0 || x >= width || y >= height ? 0 : coverage[y * width + x])
751
+ const result = Uint8Array.from(levels);
752
+ const levelAt = (x, y) =>
753
+ x < 0 || y < 0 || x >= width || y >= height ? 0 : levels[y * width + x];
754
+ const coverageAt = (x, y) =>
755
+ x < 0 || y < 0 || x >= width || y >= height ? 0 : coverage[y * width + x];
594
756
  for (let y = 0; y < height; y += 1) {
595
757
  for (let x = 0; x < width; x += 1) {
596
- const index = y * width + x
758
+ const index = y * width + x;
597
759
  // Stem solidity: a pixel enclosed by fully-inked neighbours is interior,
598
760
  // whatever rounding did to its own coverage. Forcing it to level 3 stops
599
761
  // rasterizer noise from punching grey pinholes down the middle of a stem.
@@ -604,22 +766,22 @@ function quantizeCoverageToLevels(coverage, width, height) {
604
766
  coverageAt(x, y - 1) >= 208 &&
605
767
  coverageAt(x, y + 1) >= 208
606
768
  ) {
607
- result[index] = 3
608
- continue
769
+ result[index] = 3;
770
+ continue;
609
771
  }
610
772
  // Speckle removal: a faint pixel with no ink at all around it is a stray
611
773
  // dot off a curve tip, not a stroke. On four levels it reads as dirt.
612
- if (levels[index] !== 1) continue
613
- let neighbours = 0
774
+ if (levels[index] !== 1) continue;
775
+ let neighbours = 0;
614
776
  for (let dy = -1; dy <= 1; dy += 1) {
615
777
  for (let dx = -1; dx <= 1; dx += 1) {
616
- if (dx !== 0 || dy !== 0) neighbours += levelAt(x + dx, y + dy)
778
+ if (dx !== 0 || dy !== 0) neighbours += levelAt(x + dx, y + dy);
617
779
  }
618
780
  }
619
- if (neighbours === 0) result[index] = 0
781
+ if (neighbours === 0) result[index] = 0;
620
782
  }
621
783
  }
622
- return result
784
+ return result;
623
785
  }
624
786
 
625
787
  // Pack 0..3 levels four to a byte, rows byte-aligned (stride = ceil(width / 4)).
@@ -627,48 +789,51 @@ function quantizeCoverageToLevels(coverage, width, height) {
627
789
  // dump of a row reads left to right like the glyph does. Kept in sync with
628
790
  // `RasterizedFont::coverage` in packages/engine/rasterized_font.cpp.
629
791
  function packLevelsTo2Bit(levels, width, height) {
630
- const stride = (width + 3) >> 2
631
- const packed = new Uint8Array(stride * height)
792
+ const stride = (width + 3) >> 2;
793
+ const packed = new Uint8Array(stride * height);
632
794
  for (let y = 0; y < height; y += 1) {
633
795
  for (let x = 0; x < width; x += 1) {
634
- const level = levels[y * width + x] & 0x3
635
- packed[y * stride + (x >> 2)] |= level << ((3 - (x & 3)) * 2)
796
+ const level = levels[y * width + x] & 0x3;
797
+ packed[y * stride + (x >> 2)] |= level << ((3 - (x & 3)) * 2);
636
798
  }
637
799
  }
638
- return packed
800
+ return packed;
639
801
  }
640
802
 
641
803
  function rasterizeGlyph(cmds, bitmap, gw, gh, x0, y0) {
642
- const ss = glyphSupersample
643
- const ssWidth = gw * ss
644
- const ssHeight = gh * ss
645
- const hires = new Uint8Array(ssWidth * ssHeight)
646
- const segments = pathCommandsToSegments(cmds, x0, y0)
647
- const maxEdgesPerRow = 128
648
- const scanlines = new Float32Array(ssHeight * maxEdgesPerRow)
649
- const scanCounts = new Int32Array(ssHeight)
804
+ const ss = glyphSupersample;
805
+ const ssWidth = gw * ss;
806
+ const ssHeight = gh * ss;
807
+ const hires = new Uint8Array(ssWidth * ssHeight);
808
+ const segments = pathCommandsToSegments(cmds, x0, y0);
809
+ const maxEdgesPerRow = 128;
810
+ const scanlines = new Float32Array(ssHeight * maxEdgesPerRow);
811
+ const scanCounts = new Int32Array(ssHeight);
650
812
 
651
813
  for (const [sx0, sy0, sx1, sy1] of segments) {
652
- const hsy0 = sy0 * ss
653
- const hsy1 = sy1 * ss
654
- const hsx0 = sx0 * ss
655
- const hsx1 = sx1 * ss
656
- const minRow = Math.max(0, Math.floor(Math.min(hsy0, hsy1)))
657
- const maxRow = Math.min(ssHeight - 1, Math.ceil(Math.max(hsy0, hsy1)))
814
+ const hsy0 = sy0 * ss;
815
+ const hsy1 = sy1 * ss;
816
+ const hsx0 = sx0 * ss;
817
+ const hsx1 = sx1 * ss;
818
+ const minRow = Math.max(0, Math.floor(Math.min(hsy0, hsy1)));
819
+ const maxRow = Math.min(ssHeight - 1, Math.ceil(Math.max(hsy0, hsy1)));
658
820
  for (let row = minRow; row <= maxRow; row += 1) {
659
- const y = row + 0.5
821
+ const y = row + 0.5;
660
822
  if ((hsy0 <= y && hsy1 > y) || (hsy1 <= y && hsy0 > y)) {
661
- const pos = hsx0 + ((y - hsy0) / (hsy1 - hsy0)) * (hsx1 - hsx0)
662
- const count = scanCounts[row]
663
- if (count < maxEdgesPerRow) scanlines[row * maxEdgesPerRow + scanCounts[row]++] = pos
823
+ const pos = hsx0 + ((y - hsy0) / (hsy1 - hsy0)) * (hsx1 - hsx0);
824
+ const count = scanCounts[row];
825
+ if (count < maxEdgesPerRow)
826
+ scanlines[row * maxEdgesPerRow + scanCounts[row]++] = pos;
664
827
  }
665
828
  }
666
829
  }
667
830
 
668
831
  for (let row = 0; row < ssHeight; row += 1) {
669
- const count = scanCounts[row]
670
- if (count < 2) continue
671
- const edges = Array.from(scanlines.slice(row * maxEdgesPerRow, row * maxEdgesPerRow + count)).sort((a, b) => a - b)
832
+ const count = scanCounts[row];
833
+ if (count < 2) continue;
834
+ const edges = Array.from(
835
+ scanlines.slice(row * maxEdgesPerRow, row * maxEdgesPerRow + count),
836
+ ).sort((a, b) => a - b);
672
837
  for (let i = 0; i < edges.length - 1; i += 2) {
673
838
  // At 1:1 (no supersampling) ink a pixel only when its CENTER falls inside
674
839
  // the span — the standard 50% coverage rule — so glyphs keep their true
@@ -676,195 +841,215 @@ function rasterizeGlyph(cmds, bitmap, gw, gh, x0, y0) {
676
841
  // up to a pixel per side and reads as bold. Supersampled paths keep the
677
842
  // outward rounding: it's averaged away on downsample and avoids dropping
678
843
  // thin sub-pixel coverage.
679
- const left = ss === 1
680
- ? Math.max(0, Math.ceil(edges[i] - 0.5))
681
- : Math.max(0, Math.floor(edges[i]))
682
- const right = ss === 1
683
- ? Math.min(ssWidth - 1, Math.floor(edges[i + 1] - 0.5))
684
- : Math.min(ssWidth - 1, Math.ceil(edges[i + 1]))
685
- for (let col = left; col <= right; col += 1) hires[row * ssWidth + col] = 1
844
+ const left =
845
+ ss === 1
846
+ ? Math.max(0, Math.ceil(edges[i] - 0.5))
847
+ : Math.max(0, Math.floor(edges[i]));
848
+ const right =
849
+ ss === 1
850
+ ? Math.min(ssWidth - 1, Math.floor(edges[i + 1] - 0.5))
851
+ : Math.min(ssWidth - 1, Math.ceil(edges[i + 1]));
852
+ for (let col = left; col <= right; col += 1)
853
+ hires[row * ssWidth + col] = 1;
686
854
  }
687
855
  }
688
856
 
689
857
  for (let py = 0; py < gh; py += 1) {
690
858
  for (let px = 0; px < gw; px += 1) {
691
- let sum = 0
859
+ let sum = 0;
692
860
  for (let sy = 0; sy < ss; sy += 1) {
693
- for (let sx = 0; sx < ss; sx += 1) sum += hires[(py * ss + sy) * ssWidth + (px * ss + sx)]
861
+ for (let sx = 0; sx < ss; sx += 1)
862
+ sum += hires[(py * ss + sy) * ssWidth + (px * ss + sx)];
694
863
  }
695
- bitmap[py * gw + px] = Math.round((sum / (ss * ss)) * 255)
864
+ bitmap[py * gw + px] = Math.round((sum / (ss * ss)) * 255);
696
865
  }
697
866
  }
698
867
  }
699
868
 
700
869
  function pathCommandsToSegments(cmds, x0, y0) {
701
- const segments = []
702
- let curX = 0
703
- let curY = 0
704
- let startX = 0
705
- let startY = 0
870
+ const segments = [];
871
+ let curX = 0;
872
+ let curY = 0;
873
+ let startX = 0;
874
+ let startY = 0;
706
875
 
707
876
  for (const cmd of cmds) {
708
- if (cmd.type === 'M') {
709
- curX = cmd.x - x0
710
- curY = cmd.y - y0
711
- startX = curX
712
- startY = curY
713
- } else if (cmd.type === 'L') {
714
- const endX = cmd.x - x0
715
- const endY = cmd.y - y0
716
- segments.push([curX, curY, endX, endY])
717
- curX = endX
718
- curY = endY
719
- } else if (cmd.type === 'C') {
877
+ if (cmd.type === "M") {
878
+ curX = cmd.x - x0;
879
+ curY = cmd.y - y0;
880
+ startX = curX;
881
+ startY = curY;
882
+ } else if (cmd.type === "L") {
883
+ const endX = cmd.x - x0;
884
+ const endY = cmd.y - y0;
885
+ segments.push([curX, curY, endX, endY]);
886
+ curX = endX;
887
+ curY = endY;
888
+ } else if (cmd.type === "C") {
720
889
  for (let s = 1; s <= 12; s += 1) {
721
- const t = s / 12
722
- const mt = 1 - t
723
- const endX = mt ** 3 * curX + 3 * mt * mt * t * (cmd.x1 - x0) + 3 * mt * t * t * (cmd.x2 - x0) + t ** 3 * (cmd.x - x0)
724
- const endY = mt ** 3 * curY + 3 * mt * mt * t * (cmd.y1 - y0) + 3 * mt * t * t * (cmd.y2 - y0) + t ** 3 * (cmd.y - y0)
725
- segments.push([curX, curY, endX, endY])
726
- curX = endX
727
- curY = endY
890
+ const t = s / 12;
891
+ const mt = 1 - t;
892
+ const endX =
893
+ mt ** 3 * curX +
894
+ 3 * mt * mt * t * (cmd.x1 - x0) +
895
+ 3 * mt * t * t * (cmd.x2 - x0) +
896
+ t ** 3 * (cmd.x - x0);
897
+ const endY =
898
+ mt ** 3 * curY +
899
+ 3 * mt * mt * t * (cmd.y1 - y0) +
900
+ 3 * mt * t * t * (cmd.y2 - y0) +
901
+ t ** 3 * (cmd.y - y0);
902
+ segments.push([curX, curY, endX, endY]);
903
+ curX = endX;
904
+ curY = endY;
728
905
  }
729
- } else if (cmd.type === 'Q') {
906
+ } else if (cmd.type === "Q") {
730
907
  for (let s = 1; s <= 8; s += 1) {
731
- const t = s / 8
732
- const mt = 1 - t
733
- const endX = mt * mt * curX + 2 * mt * t * (cmd.x1 - x0) + t * t * (cmd.x - x0)
734
- const endY = mt * mt * curY + 2 * mt * t * (cmd.y1 - y0) + t * t * (cmd.y - y0)
735
- segments.push([curX, curY, endX, endY])
736
- curX = endX
737
- curY = endY
908
+ const t = s / 8;
909
+ const mt = 1 - t;
910
+ const endX =
911
+ mt * mt * curX + 2 * mt * t * (cmd.x1 - x0) + t * t * (cmd.x - x0);
912
+ const endY =
913
+ mt * mt * curY + 2 * mt * t * (cmd.y1 - y0) + t * t * (cmd.y - y0);
914
+ segments.push([curX, curY, endX, endY]);
915
+ curX = endX;
916
+ curY = endY;
738
917
  }
739
- } else if (cmd.type === 'Z') {
740
- if (curX !== startX || curY !== startY) segments.push([curX, curY, startX, startY])
741
- curX = startX
742
- curY = startY
918
+ } else if (cmd.type === "Z") {
919
+ if (curX !== startX || curY !== startY)
920
+ segments.push([curX, curY, startX, startY]);
921
+ curX = startX;
922
+ curY = startY;
743
923
  }
744
924
  }
745
925
 
746
- return segments
926
+ return segments;
747
927
  }
748
928
 
749
929
  function packGlyphAtlas(glyphs) {
750
- const padding = 1
751
- const maxAtlasWidth = 512
752
- let atlasWidth = 0
753
- let curX = 0
754
- let curY = 0
755
- let maxRowHeight = 0
930
+ const padding = 1;
931
+ const maxAtlasWidth = 512;
932
+ let atlasWidth = 0;
933
+ let curX = 0;
934
+ let curY = 0;
935
+ let maxRowHeight = 0;
756
936
 
757
937
  for (const glyph of glyphs) {
758
938
  if (curX + glyph.width + padding > maxAtlasWidth) {
759
- curX = 0
760
- curY += maxRowHeight + padding
761
- maxRowHeight = 0
939
+ curX = 0;
940
+ curY += maxRowHeight + padding;
941
+ maxRowHeight = 0;
762
942
  }
763
- glyph.sourceX = curX
764
- glyph.sourceY = curY
765
- if (curX + glyph.width > atlasWidth) atlasWidth = curX + glyph.width
766
- if (glyph.height > maxRowHeight) maxRowHeight = glyph.height
767
- curX += glyph.width + padding
943
+ glyph.sourceX = curX;
944
+ glyph.sourceY = curY;
945
+ if (curX + glyph.width > atlasWidth) atlasWidth = curX + glyph.width;
946
+ if (glyph.height > maxRowHeight) maxRowHeight = glyph.height;
947
+ curX += glyph.width + padding;
768
948
  }
769
949
 
770
- return { atlasWidth, atlasHeight: curY + maxRowHeight }
950
+ return { atlasWidth, atlasHeight: curY + maxRowHeight };
771
951
  }
772
952
 
773
953
  function cppString(value) {
774
- return JSON.stringify(value)
954
+ return JSON.stringify(value);
775
955
  }
776
956
 
777
957
  function generatedHeader(fontCount, familyCount, options = {}) {
778
958
  const lines = [
779
- '#pragma once',
780
- '#define GEA_EMBEDDED_HAS_GENERATED_FONTS 1',
959
+ "#pragma once",
960
+ "#define GEA_EMBEDDED_HAS_GENERATED_FONTS 1",
781
961
  '#include "graphics/font.h"',
782
962
  `#define GEA_EMBEDDED_FONT_COUNT ${fontCount}`,
783
963
  `#define GEA_EMBEDDED_FONT_FAMILY_COUNT ${familyCount}`,
784
- ]
785
- if (options.runtimeTtfFonts) lines.push('#define GEA_EMBEDDED_HAS_RUNTIME_TTF_FONTS 1')
786
- lines.push('')
787
- return lines.join('\n')
964
+ ];
965
+ if (options.runtimeTtfFonts)
966
+ lines.push("#define GEA_EMBEDDED_HAS_RUNTIME_TTF_FONTS 1");
967
+ lines.push("");
968
+ return lines.join("\n");
788
969
  }
789
970
 
790
971
  function fontFunctionNames() {
791
972
  if (!symbolPrefix) {
792
973
  return {
793
- namespaceOpen: 'namespace gea::framework::graphics::generated {',
794
- namespaceClose: '} // namespace gea::framework::graphics::generated',
795
- ensureLinked: 'ensureLinked',
796
- lookupFont: 'lookupFont',
797
- lookupFontForFamily: 'lookupFontForFamily',
798
- lookupFontFamily: 'lookupFontFamily',
799
- lookupFontFamilyName: 'lookupFontFamilyName',
800
- lookupRuntimeTtfFontForFamily: 'lookupRuntimeTtfFontForFamily',
801
- }
974
+ namespaceOpen: "namespace gea::framework::graphics::generated {",
975
+ namespaceClose: "} // namespace gea::framework::graphics::generated",
976
+ ensureLinked: "ensureLinked",
977
+ lookupFont: "lookupFont",
978
+ lookupFontForFamily: "lookupFontForFamily",
979
+ lookupFontFamily: "lookupFontFamily",
980
+ lookupFontFamilyName: "lookupFontFamilyName",
981
+ lookupRuntimeTtfFontForFamily: "lookupRuntimeTtfFontForFamily",
982
+ };
802
983
  }
803
984
  return {
804
- namespaceOpen: '',
805
- namespaceClose: '',
985
+ namespaceOpen: "",
986
+ namespaceClose: "",
806
987
  ensureLinked: `${symbolPrefix}_ensure_linked_fonts`,
807
988
  lookupFont: `${symbolPrefix}_lookup_font`,
808
989
  lookupFontForFamily: `${symbolPrefix}_lookup_font_for_family`,
809
990
  lookupFontFamily: `${symbolPrefix}_lookup_font_family`,
810
991
  lookupFontFamilyName: `${symbolPrefix}_lookup_font_family_name`,
811
992
  lookupRuntimeTtfFontForFamily: `${symbolPrefix}_lookup_runtime_ttf_font_for_family`,
812
- }
993
+ };
813
994
  }
814
995
 
815
996
  function emitEmptyFontLookup(lines, names) {
816
- lines.push(`void ${names.ensureLinked}()`)
817
- lines.push('{')
818
- lines.push('}')
819
- lines.push('')
820
- lines.push(`const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`)
821
- lines.push('{')
822
- lines.push(' (void)fontId;')
823
- lines.push(' return nullptr;')
824
- lines.push('}')
825
- lines.push('')
826
- lines.push(`const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`)
827
- lines.push('{')
828
- lines.push(' (void)familyId;')
829
- lines.push(' (void)sizePx;')
830
- lines.push(' return nullptr;')
831
- lines.push('}')
832
- lines.push('')
833
- lines.push(`int ${names.lookupFontFamily}(const char *family)`)
834
- lines.push('{')
835
- lines.push(' (void)family;')
836
- lines.push(' return -1;')
837
- lines.push('}')
838
- lines.push('')
839
- lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`)
840
- lines.push('{')
841
- lines.push(' (void)familyId;')
842
- lines.push(' return nullptr;')
843
- lines.push('}')
844
- lines.push('')
997
+ lines.push(`void ${names.ensureLinked}()`);
998
+ lines.push("{");
999
+ lines.push("}");
1000
+ lines.push("");
1001
+ lines.push(
1002
+ `const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`,
1003
+ );
1004
+ lines.push("{");
1005
+ lines.push(" (void)fontId;");
1006
+ lines.push(" return nullptr;");
1007
+ lines.push("}");
1008
+ lines.push("");
1009
+ lines.push(
1010
+ `const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`,
1011
+ );
1012
+ lines.push("{");
1013
+ lines.push(" (void)familyId;");
1014
+ lines.push(" (void)sizePx;");
1015
+ lines.push(" return nullptr;");
1016
+ lines.push("}");
1017
+ lines.push("");
1018
+ lines.push(`int ${names.lookupFontFamily}(const char *family)`);
1019
+ lines.push("{");
1020
+ lines.push(" (void)family;");
1021
+ lines.push(" return -1;");
1022
+ lines.push("}");
1023
+ lines.push("");
1024
+ lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`);
1025
+ lines.push("{");
1026
+ lines.push(" (void)familyId;");
1027
+ lines.push(" return nullptr;");
1028
+ lines.push("}");
1029
+ lines.push("");
845
1030
  }
846
1031
 
847
1032
  function generatedCpp(fonts, families) {
848
- const names = fontFunctionNames()
1033
+ const names = fontFunctionNames();
849
1034
  const lines = [
850
1035
  '#include "gea_embedded_font_generated.h"',
851
- '#include <cstring>',
852
- '',
853
- ]
1036
+ "#include <cstring>",
1037
+ "",
1038
+ ];
854
1039
  if (names.namespaceOpen) {
855
- lines.push(names.namespaceOpen)
856
- lines.push('')
1040
+ lines.push(names.namespaceOpen);
1041
+ lines.push("");
857
1042
  } else {
858
- lines.push('using gea::framework::graphics::Glyph;')
859
- lines.push('using gea::framework::graphics::RasterizedFontData;')
860
- lines.push('')
1043
+ lines.push("using gea::framework::graphics::Glyph;");
1044
+ lines.push("using gea::framework::graphics::RasterizedFontData;");
1045
+ lines.push("");
861
1046
  }
862
1047
 
863
1048
  if (fonts.length === 0) {
864
- emitEmptyFontLookup(lines, names)
865
- if (names.namespaceClose) lines.push(names.namespaceClose)
866
- lines.push('')
867
- return lines.join('\n')
1049
+ emitEmptyFontLookup(lines, names);
1050
+ if (names.namespaceClose) lines.push(names.namespaceClose);
1051
+ lines.push("");
1052
+ return lines.join("\n");
868
1053
  }
869
1054
 
870
1055
  // Atlas bitmaps are the bulk of the font footprint and are frequently
@@ -874,237 +1059,305 @@ function generatedCpp(fonts, families) {
874
1059
  // copies into one, deduping the bundle with no runtime change. We dedup within
875
1060
  // this TU too (a weak symbol can't be defined twice in one TU); `font_data`
876
1061
  // points at the shared symbol either way.
877
- const atlasSymbols = new Map()
1062
+ const atlasSymbols = new Map();
878
1063
  for (const font of fonts) {
879
- const hash = createHash('sha1').update(Buffer.from(font.atlasData)).digest('hex').slice(0, 16)
880
- const symbol = `gea_font_atlas_${hash}`
881
- font.atlasSymbol = symbol
882
- if (atlasSymbols.has(hash)) continue
883
- atlasSymbols.set(hash, symbol)
1064
+ const hash = createHash("sha1")
1065
+ .update(Buffer.from(font.atlasData))
1066
+ .digest("hex")
1067
+ .slice(0, 16);
1068
+ const symbol = `gea_font_atlas_${hash}`;
1069
+ font.atlasSymbol = symbol;
1070
+ if (atlasSymbols.has(hash)) continue;
1071
+ atlasSymbols.set(hash, symbol);
884
1072
  // `extern` + initializer: a namespace-scope `const` is internal by default,
885
1073
  // and `weak` requires external linkage ("weak declaration must be public").
886
- lines.push(`extern __attribute__((weak)) const std::uint8_t ${symbol}[${font.atlasData.length}] = {`)
1074
+ lines.push(
1075
+ `extern __attribute__((weak)) const std::uint8_t ${symbol}[${font.atlasData.length}] = {`,
1076
+ );
887
1077
  for (let i = 0; i < font.atlasData.length; i += 32) {
888
- const chunk = Array.from(font.atlasData.slice(i, Math.min(i + 32, font.atlasData.length)))
889
- lines.push(` ${chunk.map((byte) => `0x${byte.toString(16).padStart(2, '0')}`).join(', ')},`)
1078
+ const chunk = Array.from(
1079
+ font.atlasData.slice(i, Math.min(i + 32, font.atlasData.length)),
1080
+ );
1081
+ lines.push(
1082
+ ` ${chunk.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`).join(", ")},`,
1083
+ );
890
1084
  }
891
- lines.push('};')
892
- lines.push('')
1085
+ lines.push("};");
1086
+ lines.push("");
893
1087
  }
894
1088
 
895
1089
  for (const font of fonts) {
896
1090
  // `atlasBits` defaults to 8 in RasterizedFontData, so an 8-bit atlas leaves
897
1091
  // the trailing initializer off entirely and keeps this TU byte-identical to
898
1092
  // what every board generated before 2-bit atlases existed.
899
- const atlasBitsInit = font.atlasBits === 8 ? '' : `, ${font.atlasBits}`
900
- lines.push(`static const Glyph font_glyphs_${font.id}[${font.glyphs.length}] = {`)
1093
+ const atlasBitsInit = font.atlasBits === 8 ? "" : `, ${font.atlasBits}`;
1094
+ lines.push(
1095
+ `static const Glyph font_glyphs_${font.id}[${font.glyphs.length}] = {`,
1096
+ );
901
1097
  for (const glyph of font.glyphs) {
902
1098
  lines.push(
903
- ` { ${glyph.codepoint}, ${glyph.sourceX}, ${glyph.sourceY}, ${glyph.width}, ${glyph.height}, ${glyph.advance}, ${glyph.bearingX}, ${glyph.bearingY} },`,
904
- )
1099
+ ` { ${glyph.codepoint}, ${glyph.sourceX}, ${glyph.sourceY}, ${glyph.width}, ${glyph.height}, ${glyph.advance}, ${glyph.bearingX}, ${glyph.bearingY}${glyph.advance16 === undefined ? "" : `, ${glyph.advance16}`} },`,
1100
+ );
1101
+ }
1102
+ lines.push("};");
1103
+ lines.push("");
1104
+
1105
+ let importedFields = "";
1106
+ if (font.importedBitmap) {
1107
+ if (font.kerning.length > 0) {
1108
+ lines.push(
1109
+ `static const gea::framework::graphics::FontKerningPair font_kerning_${font.id}[${font.kerning.length}] = {`,
1110
+ );
1111
+ for (const pair of font.kerning)
1112
+ lines.push(
1113
+ ` { ${pair.left}, ${pair.right}, ${pair.adjustment16} },`,
1114
+ );
1115
+ lines.push("};");
1116
+ }
1117
+ importedFields = `, 8, ${font.kerning.length}, ${font.kerning.length ? `font_kerning_${font.id}` : "nullptr"}, true, ${font.fallbackCodepoint}`;
905
1118
  }
906
- lines.push('};')
907
- lines.push('')
908
-
909
1119
  lines.push(
910
- `static const RasterizedFontData font_data_${font.id} = { ${font.id}, ${font.sizePx}, ${font.lineHeight}, ${font.ascender}, ${font.descender}, ${font.glyphs.length}, font_glyphs_${font.id}, ${font.atlasWidth}, ${font.atlasHeight}, ${font.atlasSymbol}${atlasBitsInit} };`,
911
- )
912
- lines.push('')
1120
+ `static const RasterizedFontData font_data_${font.id} = { ${font.id}, ${font.sizePx}, ${font.lineHeight}, ${font.ascender}, ${font.descender}, ${font.glyphs.length}, font_glyphs_${font.id}, ${font.atlasWidth}, ${font.atlasHeight}, ${font.atlasSymbol}${font.importedBitmap ? importedFields : atlasBitsInit} };`,
1121
+ );
1122
+ lines.push("");
913
1123
  }
914
1124
 
915
- lines.push('struct GeneratedFontEntry {')
916
- lines.push(' int familyId;')
917
- lines.push(' const RasterizedFontData *font;')
918
- lines.push('};')
919
- lines.push('')
920
- lines.push('static const GeneratedFontEntry generated_fonts[] = {')
921
- for (const font of fonts) lines.push(` { ${font.familyId}, &font_data_${font.id} },`)
922
- lines.push('};')
923
- lines.push('')
924
- lines.push('struct GeneratedFontFamily {')
925
- lines.push(' int id;')
926
- lines.push(' const char *name;')
927
- lines.push('};')
928
- lines.push('')
929
- lines.push('static const GeneratedFontFamily generated_families[] = {')
930
- for (const family of families) lines.push(` { ${family.id}, ${cppString(family.name)} },`)
931
- lines.push('};')
932
- lines.push('')
933
- lines.push(`void ${names.ensureLinked}()`)
934
- lines.push('{')
935
- lines.push('}')
936
- lines.push('')
937
- lines.push(`const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`)
938
- lines.push('{')
939
- lines.push(' for (const auto &entry : generated_fonts) {')
940
- lines.push(' if (entry.font && entry.font->id == fontId) return entry.font;')
941
- lines.push(' }')
942
- lines.push(' return nullptr;')
943
- lines.push('}')
944
- lines.push('')
945
- lines.push(`const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`)
946
- lines.push('{')
947
- lines.push(' const RasterizedFontData *best = nullptr;')
948
- lines.push(' int bestDiff = 2147483647;')
949
- lines.push(' for (const auto &entry : generated_fonts) {')
950
- lines.push(' if (entry.familyId != familyId || !entry.font) continue;')
951
- lines.push(' int diff = sizePx > 0 ? entry.font->sizePx - sizePx : 0;')
952
- lines.push(' if (diff < 0) diff = -diff;')
953
- lines.push(' if (diff < bestDiff) {')
954
- lines.push(' best = entry.font;')
955
- lines.push(' bestDiff = diff;')
956
- lines.push(' }')
957
- lines.push(' }')
958
- lines.push(' return best;')
959
- lines.push('}')
960
- lines.push('')
961
- lines.push(`int ${names.lookupFontFamily}(const char *family)`)
962
- lines.push('{')
963
- lines.push(' if (!family) return -1;')
964
- lines.push(' for (const auto &entry : generated_families) {')
965
- lines.push(' if (std::strcmp(entry.name, family) == 0) return entry.id;')
966
- lines.push(' }')
967
- lines.push(' return -1;')
968
- lines.push('}')
969
- lines.push('')
970
- lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`)
971
- lines.push('{')
972
- lines.push(' for (const auto &entry : generated_families) {')
973
- lines.push(' if (entry.id == familyId) return entry.name;')
974
- lines.push(' }')
975
- lines.push(' return nullptr;')
976
- lines.push('}')
977
- lines.push('')
978
- if (names.namespaceClose) lines.push(names.namespaceClose)
979
- lines.push('')
980
- return lines.join('\n')
1125
+ lines.push("struct GeneratedFontEntry {");
1126
+ lines.push(" int familyId;");
1127
+ lines.push(" const RasterizedFontData *font;");
1128
+ lines.push("};");
1129
+ lines.push("");
1130
+ lines.push("static const GeneratedFontEntry generated_fonts[] = {");
1131
+ for (const font of fonts)
1132
+ lines.push(` { ${font.familyId}, &font_data_${font.id} },`);
1133
+ lines.push("};");
1134
+ lines.push("");
1135
+ lines.push("struct GeneratedFontFamily {");
1136
+ lines.push(" int id;");
1137
+ lines.push(" const char *name;");
1138
+ lines.push("};");
1139
+ lines.push("");
1140
+ lines.push("static const GeneratedFontFamily generated_families[] = {");
1141
+ for (const family of families)
1142
+ lines.push(` { ${family.id}, ${cppString(family.name)} },`);
1143
+ lines.push("};");
1144
+ lines.push("");
1145
+ lines.push(`void ${names.ensureLinked}()`);
1146
+ lines.push("{");
1147
+ lines.push("}");
1148
+ lines.push("");
1149
+ lines.push(
1150
+ `const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`,
1151
+ );
1152
+ lines.push("{");
1153
+ lines.push(" for (const auto &entry : generated_fonts) {");
1154
+ lines.push(
1155
+ " if (entry.font && entry.font->id == fontId) return entry.font;",
1156
+ );
1157
+ lines.push(" }");
1158
+ lines.push(" return nullptr;");
1159
+ lines.push("}");
1160
+ lines.push("");
1161
+ lines.push(
1162
+ `const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`,
1163
+ );
1164
+ lines.push("{");
1165
+ lines.push(" const RasterizedFontData *best = nullptr;");
1166
+ lines.push(" int bestDiff = 2147483647;");
1167
+ lines.push(" for (const auto &entry : generated_fonts) {");
1168
+ lines.push(
1169
+ " if (entry.familyId != familyId || !entry.font) continue;",
1170
+ );
1171
+ lines.push(
1172
+ " int diff = sizePx > 0 ? entry.font->sizePx - sizePx : 0;",
1173
+ );
1174
+ lines.push(" if (diff < 0) diff = -diff;");
1175
+ lines.push(" if (diff < bestDiff) {");
1176
+ lines.push(" best = entry.font;");
1177
+ lines.push(" bestDiff = diff;");
1178
+ lines.push(" }");
1179
+ lines.push(" }");
1180
+ lines.push(" return best;");
1181
+ lines.push("}");
1182
+ lines.push("");
1183
+ lines.push(`int ${names.lookupFontFamily}(const char *family)`);
1184
+ lines.push("{");
1185
+ lines.push(" if (!family) return -1;");
1186
+ lines.push(" for (const auto &entry : generated_families) {");
1187
+ lines.push(
1188
+ " if (std::strcmp(entry.name, family) == 0) return entry.id;",
1189
+ );
1190
+ lines.push(" }");
1191
+ lines.push(" return -1;");
1192
+ lines.push("}");
1193
+ lines.push("");
1194
+ lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`);
1195
+ lines.push("{");
1196
+ lines.push(" for (const auto &entry : generated_families) {");
1197
+ lines.push(" if (entry.id == familyId) return entry.name;");
1198
+ lines.push(" }");
1199
+ lines.push(" return nullptr;");
1200
+ lines.push("}");
1201
+ lines.push("");
1202
+ if (names.namespaceClose) lines.push(names.namespaceClose);
1203
+ lines.push("");
1204
+ return lines.join("\n");
981
1205
  }
982
1206
 
983
1207
  function generatedRuntimeTtfCpp(runtimeFonts, families) {
984
- const names = fontFunctionNames()
1208
+ const names = fontFunctionNames();
985
1209
  const lines = [
986
1210
  '#include "gea_embedded_font_generated.h"',
987
1211
  '#include "memory.h"',
988
- '#include <cstddef>',
989
- '#include <cstring>',
990
- '',
1212
+ "#include <cstddef>",
1213
+ "#include <cstring>",
1214
+ "",
991
1215
  'extern "C" int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen);',
992
- '',
993
- ]
1216
+ "",
1217
+ ];
994
1218
  if (names.namespaceOpen) {
995
- lines.push(names.namespaceOpen)
996
- lines.push('')
1219
+ lines.push(names.namespaceOpen);
1220
+ lines.push("");
997
1221
  } else {
998
- lines.push('using gea::framework::graphics::RasterizedFontData;')
999
- lines.push('')
1222
+ lines.push("using gea::framework::graphics::RasterizedFontData;");
1223
+ lines.push("");
1000
1224
  }
1001
1225
 
1002
1226
  for (const font of runtimeFonts) {
1003
- const symbol = `gea_runtime_ttf_${font.id}`
1004
- const compressed = zlib.deflateSync(font.data, { level: 9 })
1005
- font.byteSymbol = `${symbol}_deflated`
1006
- font.byteLengthSymbol = `${symbol}_deflated_len`
1007
- font.uncompressedLength = font.data.length
1008
- lines.push(`extern __attribute__((weak)) const std::uint8_t ${font.byteSymbol}[${compressed.length}] = {`)
1227
+ const symbol = `gea_runtime_ttf_${font.id}`;
1228
+ const compressed = zlib.deflateSync(font.data, { level: 9 });
1229
+ font.byteSymbol = `${symbol}_deflated`;
1230
+ font.byteLengthSymbol = `${symbol}_deflated_len`;
1231
+ font.uncompressedLength = font.data.length;
1232
+ lines.push(
1233
+ `extern __attribute__((weak)) const std::uint8_t ${font.byteSymbol}[${compressed.length}] = {`,
1234
+ );
1009
1235
  for (let i = 0; i < compressed.length; i += 32) {
1010
- const chunk = Array.from(compressed.slice(i, Math.min(i + 32, compressed.length)))
1011
- lines.push(` ${chunk.map((byte) => `0x${byte.toString(16).padStart(2, '0')}`).join(', ')},`)
1236
+ const chunk = Array.from(
1237
+ compressed.slice(i, Math.min(i + 32, compressed.length)),
1238
+ );
1239
+ lines.push(
1240
+ ` ${chunk.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`).join(", ")},`,
1241
+ );
1012
1242
  }
1013
- lines.push('};')
1014
- lines.push(`extern __attribute__((weak)) const unsigned long ${font.byteLengthSymbol} = ${compressed.length}UL;`)
1015
- lines.push(`extern __attribute__((weak)) const unsigned long ${symbol}_len = ${font.data.length}UL;`)
1016
- lines.push('')
1243
+ lines.push("};");
1244
+ lines.push(
1245
+ `extern __attribute__((weak)) const unsigned long ${font.byteLengthSymbol} = ${compressed.length}UL;`,
1246
+ );
1247
+ lines.push(
1248
+ `extern __attribute__((weak)) const unsigned long ${symbol}_len = ${font.data.length}UL;`,
1249
+ );
1250
+ lines.push("");
1017
1251
  }
1018
1252
 
1019
- lines.push('struct GeneratedFontFamily {')
1020
- lines.push(' int id;')
1021
- lines.push(' const char *name;')
1022
- lines.push('};')
1023
- lines.push('')
1024
- lines.push('static const GeneratedFontFamily generated_families[] = {')
1025
- for (const family of families) lines.push(` { ${family.id}, ${cppString(family.name)} },`)
1026
- lines.push('};')
1027
- lines.push('')
1028
- lines.push('struct RuntimeTtfFontEntry {')
1029
- lines.push(' int familyId;')
1030
- lines.push(' const std::uint8_t *compressedBytes;')
1031
- lines.push(' const unsigned long *compressedLength;')
1032
- lines.push(' unsigned long uncompressedLength;')
1033
- lines.push(' std::uint8_t *decodedBytes;')
1034
- lines.push('};')
1035
- lines.push('')
1036
- lines.push('static RuntimeTtfFontEntry runtime_ttf_fonts[] = {')
1253
+ lines.push("struct GeneratedFontFamily {");
1254
+ lines.push(" int id;");
1255
+ lines.push(" const char *name;");
1256
+ lines.push("};");
1257
+ lines.push("");
1258
+ lines.push("static const GeneratedFontFamily generated_families[] = {");
1259
+ for (const family of families)
1260
+ lines.push(` { ${family.id}, ${cppString(family.name)} },`);
1261
+ lines.push("};");
1262
+ lines.push("");
1263
+ lines.push("struct RuntimeTtfFontEntry {");
1264
+ lines.push(" int familyId;");
1265
+ lines.push(" const std::uint8_t *compressedBytes;");
1266
+ lines.push(" const unsigned long *compressedLength;");
1267
+ lines.push(" unsigned long uncompressedLength;");
1268
+ lines.push(" std::uint8_t *decodedBytes;");
1269
+ lines.push("};");
1270
+ lines.push("");
1271
+ lines.push("static RuntimeTtfFontEntry runtime_ttf_fonts[] = {");
1037
1272
  for (const font of runtimeFonts) {
1038
- lines.push(` { ${font.familyId}, ${font.byteSymbol}, &${font.byteLengthSymbol}, ${font.uncompressedLength}UL, nullptr },`)
1273
+ lines.push(
1274
+ ` { ${font.familyId}, ${font.byteSymbol}, &${font.byteLengthSymbol}, ${font.uncompressedLength}UL, nullptr },`,
1275
+ );
1039
1276
  }
1040
- lines.push('};')
1041
- lines.push('')
1042
- lines.push('std::uint8_t *decodeRuntimeTtfFont(RuntimeTtfFontEntry &entry)')
1043
- lines.push('{')
1044
- lines.push(' if (entry.decodedBytes) return entry.decodedBytes;')
1045
- lines.push(' if (!entry.compressedBytes || !entry.compressedLength || *entry.compressedLength == 0UL || entry.uncompressedLength == 0UL) return nullptr;')
1046
- lines.push(' auto *decoded = static_cast<std::uint8_t *>(gea::framework::memory::Allocator::allocatePreferSpiram(entry.uncompressedLength, alignof(std::max_align_t)));')
1047
- lines.push(' if (!decoded) return nullptr;')
1048
- lines.push(' const int written = stbi_zlib_decode_buffer(reinterpret_cast<char *>(decoded),')
1049
- lines.push(' static_cast<int>(entry.uncompressedLength),')
1050
- lines.push(' reinterpret_cast<const char *>(entry.compressedBytes),')
1051
- lines.push(' static_cast<int>(*entry.compressedLength));')
1052
- lines.push(' if (written != static_cast<int>(entry.uncompressedLength)) {')
1053
- lines.push(' gea::framework::memory::Allocator::free(decoded);')
1054
- lines.push(' return nullptr;')
1055
- lines.push(' }')
1056
- lines.push(' entry.decodedBytes = decoded;')
1057
- lines.push(' return entry.decodedBytes;')
1058
- lines.push('}')
1059
- lines.push('')
1060
- lines.push(`void ${names.ensureLinked}()`)
1061
- lines.push('{')
1062
- lines.push('}')
1063
- lines.push('')
1064
- lines.push(`const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`)
1065
- lines.push('{')
1066
- lines.push(' (void)fontId;')
1067
- lines.push(' return nullptr;')
1068
- lines.push('}')
1069
- lines.push('')
1070
- lines.push(`const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`)
1071
- lines.push('{')
1072
- lines.push(' (void)familyId;')
1073
- lines.push(' (void)sizePx;')
1074
- lines.push(' return nullptr;')
1075
- lines.push('}')
1076
- lines.push('')
1077
- lines.push(`int ${names.lookupFontFamily}(const char *family)`)
1078
- lines.push('{')
1079
- lines.push(' if (!family) return -1;')
1080
- lines.push(' for (const auto &entry : generated_families) {')
1081
- lines.push(' if (std::strcmp(entry.name, family) == 0) return entry.id;')
1082
- lines.push(' }')
1083
- lines.push(' return -1;')
1084
- lines.push('}')
1085
- lines.push('')
1086
- lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`)
1087
- lines.push('{')
1088
- lines.push(' for (const auto &entry : generated_families) {')
1089
- lines.push(' if (entry.id == familyId) return entry.name;')
1090
- lines.push(' }')
1091
- lines.push(' return nullptr;')
1092
- lines.push('}')
1093
- lines.push('')
1094
- lines.push(`const std::uint8_t *${names.lookupRuntimeTtfFontForFamily}(int familyId, unsigned long *length)`)
1095
- lines.push('{')
1096
- lines.push(' for (auto &entry : runtime_ttf_fonts) {')
1097
- lines.push(' if (entry.familyId != familyId) continue;')
1098
- lines.push(' if (length) *length = entry.uncompressedLength;')
1099
- lines.push(' return decodeRuntimeTtfFont(entry);')
1100
- lines.push(' }')
1101
- lines.push(' if (length) *length = 0UL;')
1102
- lines.push(' return nullptr;')
1103
- lines.push('}')
1104
- lines.push('')
1105
- if (names.namespaceClose) lines.push(names.namespaceClose)
1106
- lines.push('')
1107
- return lines.join('\n')
1277
+ lines.push("};");
1278
+ lines.push("");
1279
+ lines.push("std::uint8_t *decodeRuntimeTtfFont(RuntimeTtfFontEntry &entry)");
1280
+ lines.push("{");
1281
+ lines.push(" if (entry.decodedBytes) return entry.decodedBytes;");
1282
+ lines.push(
1283
+ " if (!entry.compressedBytes || !entry.compressedLength || *entry.compressedLength == 0UL || entry.uncompressedLength == 0UL) return nullptr;",
1284
+ );
1285
+ lines.push(
1286
+ " auto *decoded = static_cast<std::uint8_t *>(gea::framework::memory::Allocator::allocatePreferSpiram(entry.uncompressedLength, alignof(std::max_align_t)));",
1287
+ );
1288
+ lines.push(" if (!decoded) return nullptr;");
1289
+ lines.push(
1290
+ " const int written = stbi_zlib_decode_buffer(reinterpret_cast<char *>(decoded),",
1291
+ );
1292
+ lines.push(" static_cast<int>(entry.uncompressedLength),");
1293
+ lines.push(" reinterpret_cast<const char *>(entry.compressedBytes),");
1294
+ lines.push(" static_cast<int>(*entry.compressedLength));");
1295
+ lines.push(
1296
+ " if (written != static_cast<int>(entry.uncompressedLength)) {",
1297
+ );
1298
+ lines.push(" gea::framework::memory::Allocator::free(decoded);");
1299
+ lines.push(" return nullptr;");
1300
+ lines.push(" }");
1301
+ lines.push(" entry.decodedBytes = decoded;");
1302
+ lines.push(" return entry.decodedBytes;");
1303
+ lines.push("}");
1304
+ lines.push("");
1305
+ lines.push(`void ${names.ensureLinked}()`);
1306
+ lines.push("{");
1307
+ lines.push("}");
1308
+ lines.push("");
1309
+ lines.push(
1310
+ `const gea::framework::graphics::RasterizedFontData *${names.lookupFont}(int fontId)`,
1311
+ );
1312
+ lines.push("{");
1313
+ lines.push(" (void)fontId;");
1314
+ lines.push(" return nullptr;");
1315
+ lines.push("}");
1316
+ lines.push("");
1317
+ lines.push(
1318
+ `const gea::framework::graphics::RasterizedFontData *${names.lookupFontForFamily}(int familyId, int sizePx)`,
1319
+ );
1320
+ lines.push("{");
1321
+ lines.push(" (void)familyId;");
1322
+ lines.push(" (void)sizePx;");
1323
+ lines.push(" return nullptr;");
1324
+ lines.push("}");
1325
+ lines.push("");
1326
+ lines.push(`int ${names.lookupFontFamily}(const char *family)`);
1327
+ lines.push("{");
1328
+ lines.push(" if (!family) return -1;");
1329
+ lines.push(" for (const auto &entry : generated_families) {");
1330
+ lines.push(
1331
+ " if (std::strcmp(entry.name, family) == 0) return entry.id;",
1332
+ );
1333
+ lines.push(" }");
1334
+ lines.push(" return -1;");
1335
+ lines.push("}");
1336
+ lines.push("");
1337
+ lines.push(`const char *${names.lookupFontFamilyName}(int familyId)`);
1338
+ lines.push("{");
1339
+ lines.push(" for (const auto &entry : generated_families) {");
1340
+ lines.push(" if (entry.id == familyId) return entry.name;");
1341
+ lines.push(" }");
1342
+ lines.push(" return nullptr;");
1343
+ lines.push("}");
1344
+ lines.push("");
1345
+ lines.push(
1346
+ `const std::uint8_t *${names.lookupRuntimeTtfFontForFamily}(int familyId, unsigned long *length)`,
1347
+ );
1348
+ lines.push("{");
1349
+ lines.push(" for (auto &entry : runtime_ttf_fonts) {");
1350
+ lines.push(" if (entry.familyId != familyId) continue;");
1351
+ lines.push(" if (length) *length = entry.uncompressedLength;");
1352
+ lines.push(" return decodeRuntimeTtfFont(entry);");
1353
+ lines.push(" }");
1354
+ lines.push(" if (length) *length = 0UL;");
1355
+ lines.push(" return nullptr;");
1356
+ lines.push("}");
1357
+ lines.push("");
1358
+ if (names.namespaceClose) lines.push(names.namespaceClose);
1359
+ lines.push("");
1360
+ return lines.join("\n");
1108
1361
  }
1109
1362
 
1110
1363
  // These TUs are megabytes of generated byte arrays that no human reads, and
@@ -1113,28 +1366,35 @@ function generatedRuntimeTtfCpp(runtimeFonts, families) {
1113
1366
  // CI runner, because the stamp below only helps a machine that already paid
1114
1367
  // it once). Formatting is therefore opt-in: set GEATSC_FORMAT_CPP=1 when the
1115
1368
  // generated output is being read by a person.
1116
- const CPP_FORMAT_EXTENSIONS = /\.(?:c|cc|cpp|cxx|m|mm|h|hh|hpp|hxx)$/i
1117
- const CPP_FORMAT_STYLE = '{BasedOnStyle: LLVM, SortIncludes: false, ColumnLimit: 160}'
1118
- let clangFormatUnavailable = false
1369
+ const CPP_FORMAT_EXTENSIONS = /\.(?:c|cc|cpp|cxx|m|mm|h|hh|hpp|hxx)$/i;
1370
+ const CPP_FORMAT_STYLE =
1371
+ "{BasedOnStyle: LLVM, SortIncludes: false, ColumnLimit: 160}";
1372
+ let clangFormatUnavailable = false;
1119
1373
 
1120
1374
  function formatCppSourceIfNeeded(filePath, contents) {
1121
- if (!CPP_FORMAT_EXTENSIONS.test(filePath)) return contents
1122
- if (process.env.GEATSC_FORMAT_CPP !== '1') return contents
1123
- if (clangFormatUnavailable) return contents
1124
- const clangFormat = process.env.CLANG_FORMAT || 'clang-format'
1375
+ if (!CPP_FORMAT_EXTENSIONS.test(filePath)) return contents;
1376
+ if (process.env.GEATSC_FORMAT_CPP !== "1") return contents;
1377
+ if (clangFormatUnavailable) return contents;
1378
+ const clangFormat = process.env.CLANG_FORMAT || "clang-format";
1125
1379
  try {
1126
- return execFileSync(clangFormat, [`--style=${CPP_FORMAT_STYLE}`, `--assume-filename=${filePath}`], {
1127
- input: contents,
1128
- encoding: 'utf8',
1129
- maxBuffer: 512 * 1024 * 1024
1130
- })
1380
+ return execFileSync(
1381
+ clangFormat,
1382
+ [`--style=${CPP_FORMAT_STYLE}`, `--assume-filename=${filePath}`],
1383
+ {
1384
+ input: contents,
1385
+ encoding: "utf8",
1386
+ maxBuffer: 512 * 1024 * 1024,
1387
+ },
1388
+ );
1131
1389
  } catch (error) {
1132
- if (error?.code === 'ENOENT') {
1133
- clangFormatUnavailable = true
1134
- process.stderr.write('generate-gea-embedded-fonts: clang-format not found; generated C++ will be written unformatted\n')
1135
- return contents
1390
+ if (error?.code === "ENOENT") {
1391
+ clangFormatUnavailable = true;
1392
+ process.stderr.write(
1393
+ "generate-gea-embedded-fonts: clang-format not found; generated C++ will be written unformatted\n",
1394
+ );
1395
+ return contents;
1136
1396
  }
1137
- throw error
1397
+ throw error;
1138
1398
  }
1139
1399
  }
1140
1400
 
@@ -1152,103 +1412,159 @@ function formatCppSourceIfNeeded(filePath, contents) {
1152
1412
  // re-running clang-format would reproduce exactly the bytes already there.
1153
1413
  // Anything else (missing or stale stamp, edited file, different clang-format)
1154
1414
  // falls back to the original format-then-compare path.
1155
- const FORMAT_STAMP_VERSION = 1
1156
- let clangFormatIdentity
1415
+ const FORMAT_STAMP_VERSION = 1;
1416
+ let clangFormatIdentity;
1157
1417
 
1158
1418
  function sha256(value) {
1159
- return createHash('sha256').update(value, typeof value === 'string' ? 'utf8' : undefined).digest('hex')
1419
+ return createHash("sha256")
1420
+ .update(value, typeof value === "string" ? "utf8" : undefined)
1421
+ .digest("hex");
1160
1422
  }
1161
1423
 
1162
1424
  function formatterIdentity() {
1163
- if (clangFormatIdentity !== undefined) return clangFormatIdentity
1164
- const clangFormat = process.env.CLANG_FORMAT || 'clang-format'
1425
+ if (clangFormatIdentity !== undefined) return clangFormatIdentity;
1426
+ const clangFormat = process.env.CLANG_FORMAT || "clang-format";
1165
1427
  try {
1166
- const version = execFileSync(clangFormat, ['--version'], { encoding: 'utf8' }).trim()
1167
- clangFormatIdentity = `${clangFormat} ${version} ${CPP_FORMAT_STYLE}`
1428
+ const version = execFileSync(clangFormat, ["--version"], {
1429
+ encoding: "utf8",
1430
+ }).trim();
1431
+ clangFormatIdentity = `${clangFormat} ${version} ${CPP_FORMAT_STYLE}`;
1168
1432
  } catch {
1169
- clangFormatIdentity = ''
1433
+ clangFormatIdentity = "";
1170
1434
  }
1171
- return clangFormatIdentity
1435
+ return clangFormatIdentity;
1172
1436
  }
1173
1437
 
1174
1438
  function formatStampPath(filePath) {
1175
- return path.join(path.dirname(filePath), `.${path.basename(filePath)}.fmtstamp`)
1439
+ return path.join(
1440
+ path.dirname(filePath),
1441
+ `.${path.basename(filePath)}.fmtstamp`,
1442
+ );
1176
1443
  }
1177
1444
 
1178
1445
  function writeFileIfChanged(filePath, contents) {
1179
- const formats = CPP_FORMAT_EXTENSIONS.test(filePath) && process.env.GEATSC_FORMAT_CPP === '1' && !clangFormatUnavailable
1446
+ const formats =
1447
+ CPP_FORMAT_EXTENSIONS.test(filePath) &&
1448
+ process.env.GEATSC_FORMAT_CPP === "1" &&
1449
+ !clangFormatUnavailable;
1180
1450
  if (!formats) {
1181
- if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === contents) return false
1182
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
1183
- fs.writeFileSync(filePath, contents)
1184
- return true
1451
+ if (
1452
+ fs.existsSync(filePath) &&
1453
+ fs.readFileSync(filePath, "utf8") === contents
1454
+ )
1455
+ return false;
1456
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1457
+ fs.writeFileSync(filePath, contents);
1458
+ return true;
1185
1459
  }
1186
1460
 
1187
- const stampPath = formatStampPath(filePath)
1188
- const rawHash = sha256(contents)
1189
- const identity = formatterIdentity()
1461
+ const stampPath = formatStampPath(filePath);
1462
+ const rawHash = sha256(contents);
1463
+ const identity = formatterIdentity();
1190
1464
  if (identity) {
1191
- let stamp
1465
+ let stamp;
1192
1466
  try {
1193
- stamp = JSON.parse(fs.readFileSync(stampPath, 'utf8'))
1467
+ stamp = JSON.parse(fs.readFileSync(stampPath, "utf8"));
1194
1468
  } catch {
1195
- stamp = undefined
1469
+ stamp = undefined;
1196
1470
  }
1197
- if (stamp?.version === FORMAT_STAMP_VERSION && stamp.raw === rawHash && stamp.identity === identity) {
1471
+ if (
1472
+ stamp?.version === FORMAT_STAMP_VERSION &&
1473
+ stamp.raw === rawHash &&
1474
+ stamp.identity === identity
1475
+ ) {
1198
1476
  try {
1199
- if (sha256(fs.readFileSync(filePath)) === stamp.formatted) return false
1477
+ if (sha256(fs.readFileSync(filePath)) === stamp.formatted) return false;
1200
1478
  } catch {
1201
1479
  // fall through and format
1202
1480
  }
1203
1481
  }
1204
1482
  }
1205
1483
 
1206
- const formattedContents = formatCppSourceIfNeeded(filePath, contents)
1207
- const changed = !fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8') !== formattedContents
1484
+ const formattedContents = formatCppSourceIfNeeded(filePath, contents);
1485
+ const changed =
1486
+ !fs.existsSync(filePath) ||
1487
+ fs.readFileSync(filePath, "utf8") !== formattedContents;
1208
1488
  if (changed) {
1209
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
1210
- fs.writeFileSync(filePath, formattedContents)
1489
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1490
+ fs.writeFileSync(filePath, formattedContents);
1211
1491
  }
1212
1492
  if (identity) {
1213
1493
  try {
1214
1494
  fs.writeFileSync(
1215
1495
  stampPath,
1216
- JSON.stringify({ version: FORMAT_STAMP_VERSION, raw: rawHash, formatted: sha256(formattedContents), identity })
1217
- )
1496
+ JSON.stringify({
1497
+ version: FORMAT_STAMP_VERSION,
1498
+ raw: rawHash,
1499
+ formatted: sha256(formattedContents),
1500
+ identity,
1501
+ }),
1502
+ );
1218
1503
  } catch {
1219
1504
  // a stamp we cannot write only costs the next build the formatting pass
1220
1505
  }
1221
1506
  }
1222
- return changed
1507
+ return changed;
1223
1508
  }
1224
1509
 
1225
1510
  function writeGeneratedFonts() {
1226
- const fontFaces = collectSourceFontFaces()
1227
- const tuples = collectUsedFontTuples(fontFaces)
1228
- const familyNames = [...new Set(tuples.map((tuple) => tuple.family))].sort()
1229
- const families = familyNames.map((name, id) => ({ id, name }))
1230
- const familyIds = new Map(families.map((family) => [family.name, family.id]))
1231
-
1232
- let fonts = []
1233
- let runtimeFonts = []
1511
+ const fontFaces = collectSourceFontFaces();
1512
+ const tuples = collectUsedFontTuples(fontFaces);
1513
+ const familyNames = [...new Set(tuples.map((tuple) => tuple.family))].sort();
1514
+ const families = familyNames.map((name, id) => ({ id, name }));
1515
+ const familyIds = new Map(families.map((family) => [family.name, family.id]));
1516
+
1517
+ let fonts = [];
1518
+ let runtimeFonts = [];
1234
1519
  if (runtimeTtfFonts) {
1235
1520
  runtimeFonts = families.map((family) => {
1236
- const fontPath = fontFaces.get(family.name)
1237
- if (!fontPath) fail(`missing @font-face src for font family ${family.name}`)
1238
- return { id: family.id, familyId: family.id, family: family.name, data: fs.readFileSync(fontPath) }
1239
- })
1521
+ const fontPath = fontFaces.get(family.name);
1522
+ if (!fontPath)
1523
+ fail(`missing @font-face src for font family ${family.name}`);
1524
+ return {
1525
+ id: family.id,
1526
+ familyId: family.id,
1527
+ family: family.name,
1528
+ data: fs.readFileSync(fontPath),
1529
+ };
1530
+ });
1240
1531
  } else if (tuples.length > 0) {
1241
- const opentype = loadOpentype()
1242
- let fontId = 0
1532
+ const opentype = loadOpentype();
1533
+ let fontId = 0;
1243
1534
  fonts = tuples.map((tuple) => {
1244
- const fontPath = fontFaces.get(tuple.family)
1245
- if (!fontPath) fail(`missing @font-face src for font family ${tuple.family}`)
1246
- return rasterizeFont(opentype, fontPath, tuple.family, familyIds.get(tuple.family), tuple.sizePx, fontId++, tuple.charset)
1247
- })
1535
+ const fontPath = fontFaces.get(tuple.family);
1536
+ if (!fontPath)
1537
+ fail(`missing @font-face src for font family ${tuple.family}`);
1538
+ const embedded = fontFaces.embeddedSources.get(tuple.family);
1539
+ if (embedded) {
1540
+ return buildBitmapFont(
1541
+ loadBitmapFontSource(embedded),
1542
+ { ...tuple, id: fontId++, familyId: familyIds.get(tuple.family) },
1543
+ packGlyphAtlas,
1544
+ );
1545
+ }
1546
+ return rasterizeFont(
1547
+ opentype,
1548
+ fontPath,
1549
+ tuple.family,
1550
+ familyIds.get(tuple.family),
1551
+ tuple.sizePx,
1552
+ fontId++,
1553
+ tuple.charset,
1554
+ );
1555
+ });
1248
1556
  }
1249
1557
 
1250
- writeFileIfChanged(outH, generatedHeader(fonts.length, families.length, { runtimeTtfFonts }))
1251
- writeFileIfChanged(outCpp, runtimeTtfFonts ? generatedRuntimeTtfCpp(runtimeFonts, families) : generatedCpp(fonts, families))
1558
+ writeFileIfChanged(
1559
+ outH,
1560
+ generatedHeader(fonts.length, families.length, { runtimeTtfFonts }),
1561
+ );
1562
+ writeFileIfChanged(
1563
+ outCpp,
1564
+ runtimeTtfFonts
1565
+ ? generatedRuntimeTtfCpp(runtimeFonts, families)
1566
+ : generatedCpp(fonts, families),
1567
+ );
1252
1568
  }
1253
1569
 
1254
- writeGeneratedFonts()
1570
+ writeGeneratedFonts();