@weasel-js/font 1.7.0 → 1.7.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.
@@ -1,5 +1,82 @@
1
1
  import { createReflectable } from '@weasel-js/registry';
2
2
 
3
+ // src/faceMetrics.ts
4
+ var finite = (n) => typeof n === "number" && Number.isFinite(n);
5
+ var USE_TYPO_METRICS = 1 << 7;
6
+ function verticalMetricsFromTables(t) {
7
+ const upem = t.unitsPerEm;
8
+ if (!finite(upem) || upem <= 0) return void 0;
9
+ const os2 = t.os2;
10
+ const pair = (a, d, down) => finite(a) && finite(d) && a + (down ? d : -d) > 0 ? { ascent: a / upem, descent: (down ? d : -d) / upem } : void 0;
11
+ const typo = pair(os2?.sTypoAscender, os2?.sTypoDescender, false);
12
+ const hhea = pair(t.hhea?.ascender, t.hhea?.descender, false);
13
+ const win = pair(os2?.usWinAscent, os2?.usWinDescent, true);
14
+ const useTypo = finite(os2?.fsSelection) && (os2.fsSelection & USE_TYPO_METRICS) !== 0;
15
+ return (useTypo ? typo : void 0) ?? hhea ?? typo ?? win;
16
+ }
17
+ function faceMetricsFromTables(t) {
18
+ const upem = t.unitsPerEm;
19
+ if (!finite(upem) || upem <= 0) return void 0;
20
+ const out = {};
21
+ const rule = (position, thickness) => finite(position) && finite(thickness) && thickness > 0 ? { offset: -position / upem, thickness: thickness / upem } : void 0;
22
+ const script = (size, shift) => finite(size) && finite(shift) && size > 0 ? { size: size / upem, shift: shift / upem } : void 0;
23
+ const underline = rule(t.post?.underlinePosition, t.post?.underlineThickness);
24
+ const strikethrough = rule(t.os2?.yStrikeoutPosition, t.os2?.yStrikeoutSize);
25
+ const superscript = script(t.os2?.ySuperscriptYSize, t.os2?.ySuperscriptYOffset);
26
+ const sub = t.os2?.ySubscriptYOffset;
27
+ const subscript = script(t.os2?.ySubscriptYSize, finite(sub) ? -sub : sub);
28
+ const vertical = verticalMetricsFromTables(t);
29
+ if (vertical) {
30
+ out.ascent = vertical.ascent;
31
+ out.descent = vertical.descent;
32
+ }
33
+ if (underline) out.underline = underline;
34
+ if (strikethrough) out.strikethrough = strikethrough;
35
+ if (superscript) out.superscript = superscript;
36
+ if (subscript) out.subscript = subscript;
37
+ const xHeight = t.os2?.sxHeight;
38
+ const capHeight = t.os2?.sCapHeight;
39
+ if (finite(xHeight) && xHeight > 0) out.xHeight = xHeight / upem;
40
+ if (finite(capHeight) && capHeight > 0) out.capHeight = capHeight / upem;
41
+ return Object.keys(out).length > 0 ? out : void 0;
42
+ }
43
+ function parseFaceMetrics(raw) {
44
+ if (raw === void 0) return void 0;
45
+ if (typeof raw !== "object" || raw === null) throw new Error("parseBmFont: faceMetrics must be an object");
46
+ const r = raw;
47
+ const out = {};
48
+ if (r.ascent !== void 0 || r.descent !== void 0) {
49
+ if (!finite(r.ascent) || !finite(r.descent) || r.ascent + r.descent <= 0) {
50
+ throw new Error("parseBmFont: malformed faceMetrics.ascent/descent");
51
+ }
52
+ out.ascent = r.ascent;
53
+ out.descent = r.descent;
54
+ }
55
+ const pair = (key, a, b, positive) => {
56
+ const v = r[key];
57
+ if (v === void 0) return;
58
+ const e = v;
59
+ if (typeof e !== "object" || e === null || !finite(e[a]) || !finite(e[b]) || e[positive] <= 0) {
60
+ throw new Error(`parseBmFont: malformed faceMetrics.${key}`);
61
+ }
62
+ out[key] = { [a]: e[a], [b]: e[b] };
63
+ };
64
+ pair("underline", "offset", "thickness", "thickness");
65
+ pair("strikethrough", "offset", "thickness", "thickness");
66
+ pair("superscript", "size", "shift", "size");
67
+ pair("subscript", "size", "shift", "size");
68
+ for (const key of ["xHeight", "capHeight"]) {
69
+ const v = r[key];
70
+ if (v === void 0) continue;
71
+ if (!finite(v) || v <= 0) throw new Error(`parseBmFont: malformed faceMetrics.${key}`);
72
+ out[key] = v;
73
+ }
74
+ return Object.keys(out).length > 0 ? out : void 0;
75
+ }
76
+ function faceMetricsOf(result) {
77
+ return result.outlineFace?.faceMetrics ?? result.entry?.font.faceMetrics;
78
+ }
79
+
3
80
  // src/FontAtlas.ts
4
81
  var FIXTURE_FONT = {
5
82
  info: { face: "Inter", size: 32 },
@@ -33,7 +110,18 @@ function parseBmFont(raw) {
33
110
  }
34
111
  inner.set(k.second, k.amount);
35
112
  }
36
- return { info, common, chars, kernings, charMap, kerningMap };
113
+ const faceMetrics = parseFaceMetrics(r.faceMetrics);
114
+ const range = r.distanceField?.distanceRange;
115
+ return {
116
+ info,
117
+ common,
118
+ chars,
119
+ kernings,
120
+ charMap,
121
+ kerningMap,
122
+ ...faceMetrics ? { faceMetrics } : {},
123
+ ...typeof range === "number" && range > 0 ? { distanceRange: range } : {}
124
+ };
37
125
  }
38
126
 
39
127
  // src/glyphReady.ts
@@ -60,13 +148,17 @@ function _clearGlyphReadySubscribers() {
60
148
  var policy = "substitute";
61
149
  var defaultFamily = null;
62
150
  function setFontFallbackPolicy(next) {
151
+ if (next === policy) return;
63
152
  policy = next;
153
+ notifyGlyphReady();
64
154
  }
65
155
  function getFontFallbackPolicy() {
66
156
  return policy;
67
157
  }
68
158
  function setDefaultFontFamily(family) {
159
+ if (family === defaultFamily) return;
69
160
  defaultFamily = family;
161
+ notifyGlyphReady();
70
162
  }
71
163
  function getDefaultFontFamily() {
72
164
  return defaultFamily;
@@ -205,8 +297,9 @@ function alphaToSdf(alpha, width, height, radius, cutoff) {
205
297
  // src/dynamic/glyphRasterizer.ts
206
298
  var BAKE_SIZE = 48;
207
299
  var PAD = 8;
208
- function cssFontString(weight, style, family) {
209
- return `${style === "italic" ? "italic " : ""}${weight} ${BAKE_SIZE}px ${JSON.stringify(family)}`;
300
+ var METRICS_SIZE = 1e3;
301
+ function cssFontString(weight, style, family, size = BAKE_SIZE) {
302
+ return `${style === "italic" ? "italic " : ""}${weight} ${size}px ${JSON.stringify(family)}`;
210
303
  }
211
304
  function createCanvasRasterizer() {
212
305
  let canvas;
@@ -221,18 +314,19 @@ function createCanvasRasterizer() {
221
314
  }
222
315
  const ctx = canvas.getContext("2d", { willReadFrequently: true });
223
316
  if (!ctx) throw new Error("weasel DynamicGlyphAtlas: 2D context unavailable");
224
- function setFont(family, weight, style) {
225
- ctx.font = cssFontString(weight, style, family);
317
+ function setFont(family, weight, style, size = BAKE_SIZE) {
318
+ ctx.font = cssFontString(weight, style, family, size);
226
319
  ctx.textBaseline = "alphabetic";
227
320
  ctx.textAlign = "left";
228
321
  }
229
322
  return {
230
323
  faceMetrics(family, weight, style) {
231
- setFont(family, weight, style);
324
+ setFont(family, weight, style, METRICS_SIZE);
232
325
  const m = ctx.measureText("Hg");
326
+ const k = BAKE_SIZE / METRICS_SIZE;
233
327
  return {
234
- ascent: m.fontBoundingBoxAscent ?? BAKE_SIZE * 0.8,
235
- descent: m.fontBoundingBoxDescent ?? BAKE_SIZE * 0.2
328
+ ascent: (m.fontBoundingBoxAscent ?? METRICS_SIZE * 0.8) * k,
329
+ descent: (m.fontBoundingBoxDescent ?? METRICS_SIZE * 0.2) * k
236
330
  };
237
331
  },
238
332
  rasterize(family, weight, style, codepoint) {
@@ -294,8 +388,10 @@ function getRasterizer() {
294
388
  return rasterizer;
295
389
  }
296
390
  function registerCanvasFont(family) {
391
+ if (isExplicitCanvasFont(family)) return;
297
392
  canvasFamilies.add(family);
298
393
  autoEnrolledFamilies.delete(family);
394
+ notifyGlyphReady();
299
395
  }
300
396
  function isCanvasFont(family) {
301
397
  if (!canvasFamilies.has(family)) return false;
@@ -318,11 +414,13 @@ function isExplicitCanvasFont(family) {
318
414
  return canvasFamilies.has(family) && !autoEnrolledFamilies.has(family);
319
415
  }
320
416
  function unregisterCanvasFont(family) {
417
+ if (!canvasFamilies.has(family)) return;
321
418
  canvasFamilies.delete(family);
322
419
  autoEnrolledFamilies.delete(family);
323
420
  for (const key of [...faces.keys()]) {
324
421
  if (faces.get(key).family === family) faces.delete(key);
325
422
  }
423
+ notifyGlyphReady();
326
424
  }
327
425
  function getDynamicFace(family, weight, style) {
328
426
  const key = `${family}|${weight}|${style}`;
@@ -343,7 +441,9 @@ function getDynamicFace(family, weight, style) {
343
441
  kernings: [],
344
442
  // no kerning in v1 (measured-pair kerning is future work)
345
443
  charMap: /* @__PURE__ */ new Map(),
346
- kerningMap: /* @__PURE__ */ new Map()
444
+ kerningMap: /* @__PURE__ */ new Map(),
445
+ // Unrounded, unlike `common`: layout places the baseline from these.
446
+ faceMetrics: { ascent: metrics.ascent / BAKE_SIZE, descent: metrics.descent / BAKE_SIZE }
347
447
  };
348
448
  const face = {
349
449
  family,
@@ -600,6 +700,163 @@ function sfntFromCollection(bytes, postScriptName) {
600
700
  return { bytes: extractFont(bytes, view, offsets[0]), matched: !postScriptName };
601
701
  }
602
702
 
703
+ // src/outline/gposKerning.ts
704
+ function gposPairKerning(bytes) {
705
+ const v = new DataView(bytes);
706
+ const version = v.byteLength >= 12 ? tag(v, 0) : "";
707
+ if (version !== "\0\0\0" && version !== "true" && version !== "OTTO") return null;
708
+ const gpos = tableOffset(v, "GPOS");
709
+ if (gpos === null) return null;
710
+ const featureList = gpos + v.getUint16(gpos + 6);
711
+ const lookupList = gpos + v.getUint16(gpos + 8);
712
+ const lookupIndexes = /* @__PURE__ */ new Set();
713
+ const featureCount = v.getUint16(featureList);
714
+ for (let i = 0; i < featureCount; i++) {
715
+ const rec = featureList + 2 + i * 6;
716
+ if (tag(v, rec) !== "kern") continue;
717
+ const feature = featureList + v.getUint16(rec + 4);
718
+ const count = v.getUint16(feature + 2);
719
+ for (let j = 0; j < count; j++) lookupIndexes.add(v.getUint16(feature + 4 + j * 2));
720
+ }
721
+ if (lookupIndexes.size === 0) return null;
722
+ const lookups = [];
723
+ for (const index of [...lookupIndexes].sort((a, b) => a - b)) {
724
+ const lookup = lookupList + v.getUint16(lookupList + 2 + index * 2);
725
+ const type = v.getUint16(lookup);
726
+ const subtables = [];
727
+ const count = v.getUint16(lookup + 4);
728
+ for (let j = 0; j < count; j++) {
729
+ let sub = lookup + v.getUint16(lookup + 6 + j * 2);
730
+ let subType = type;
731
+ if (type === 9) {
732
+ subType = v.getUint16(sub + 2);
733
+ sub += v.getUint32(sub + 4);
734
+ }
735
+ if (subType === 2) subtables.push(sub);
736
+ }
737
+ if (subtables.length > 0) lookups.push(subtables);
738
+ }
739
+ const memo = /* @__PURE__ */ new Map();
740
+ return (left, right) => {
741
+ const key = left * 65536 + right;
742
+ const hit = memo.get(key);
743
+ if (hit !== void 0) return hit;
744
+ let total = 0;
745
+ for (const subtables of lookups) {
746
+ for (const sub of subtables) {
747
+ const value = pairValue(v, sub, left, right);
748
+ if (value !== null) {
749
+ total += value;
750
+ break;
751
+ }
752
+ }
753
+ }
754
+ memo.set(key, total);
755
+ return total;
756
+ };
757
+ }
758
+ function tag(v, at) {
759
+ return String.fromCharCode(v.getUint8(at), v.getUint8(at + 1), v.getUint8(at + 2), v.getUint8(at + 3));
760
+ }
761
+ function tableOffset(v, name) {
762
+ const count = v.getUint16(4);
763
+ for (let i = 0; i < count; i++) {
764
+ const rec = 12 + i * 16;
765
+ if (tag(v, rec) === name) return v.getUint32(rec + 8);
766
+ }
767
+ return null;
768
+ }
769
+ function valueSize(format) {
770
+ let n = 0;
771
+ for (let f = format; f; f >>= 1) n += f & 1;
772
+ return n * 2;
773
+ }
774
+ function xAdvance(v, at, format) {
775
+ if (!(format & 4)) return 0;
776
+ return v.getInt16(at + (format & 1 ? 2 : 0) + (format & 2 ? 2 : 0));
777
+ }
778
+ function pairValue(v, sub, left, right) {
779
+ const format = v.getUint16(sub);
780
+ const covIndex = coverageIndex(v, sub + v.getUint16(sub + 2), left);
781
+ if (covIndex < 0) return null;
782
+ const vf1 = v.getUint16(sub + 4);
783
+ const vf2 = v.getUint16(sub + 6);
784
+ const recordSize = 2 + valueSize(vf1) + valueSize(vf2);
785
+ if (format === 1) {
786
+ const pairSet = sub + v.getUint16(sub + 10 + covIndex * 2);
787
+ let lo = 0;
788
+ let hi = v.getUint16(pairSet) - 1;
789
+ while (lo <= hi) {
790
+ const mid = lo + hi >> 1;
791
+ const rec = pairSet + 2 + mid * recordSize;
792
+ const second = v.getUint16(rec);
793
+ if (second === right) return xAdvance(v, rec + 2, vf1);
794
+ if (second < right) lo = mid + 1;
795
+ else hi = mid - 1;
796
+ }
797
+ return null;
798
+ }
799
+ if (format === 2) {
800
+ const class1 = classOf(v, sub + v.getUint16(sub + 8), left);
801
+ const class2 = classOf(v, sub + v.getUint16(sub + 10), right);
802
+ const class1Count = v.getUint16(sub + 12);
803
+ const class2Count = v.getUint16(sub + 14);
804
+ if (class1 >= class1Count || class2 >= class2Count) return null;
805
+ const rec = sub + 16 + (class1 * class2Count + class2) * (recordSize - 2);
806
+ return xAdvance(v, rec, vf1);
807
+ }
808
+ return null;
809
+ }
810
+ function coverageIndex(v, cov, glyph) {
811
+ const format = v.getUint16(cov);
812
+ const count = v.getUint16(cov + 2);
813
+ let lo = 0;
814
+ let hi = count - 1;
815
+ if (format === 1) {
816
+ while (lo <= hi) {
817
+ const mid = lo + hi >> 1;
818
+ const g = v.getUint16(cov + 4 + mid * 2);
819
+ if (g === glyph) return mid;
820
+ if (g < glyph) lo = mid + 1;
821
+ else hi = mid - 1;
822
+ }
823
+ return -1;
824
+ }
825
+ if (format === 2) {
826
+ while (lo <= hi) {
827
+ const mid = lo + hi >> 1;
828
+ const rec = cov + 4 + mid * 6;
829
+ const start = v.getUint16(rec);
830
+ const end = v.getUint16(rec + 2);
831
+ if (glyph < start) hi = mid - 1;
832
+ else if (glyph > end) lo = mid + 1;
833
+ else return v.getUint16(rec + 4) + glyph - start;
834
+ }
835
+ }
836
+ return -1;
837
+ }
838
+ function classOf(v, def, glyph) {
839
+ const format = v.getUint16(def);
840
+ if (format === 1) {
841
+ const start = v.getUint16(def + 2);
842
+ const count = v.getUint16(def + 4);
843
+ const i = glyph - start;
844
+ return i >= 0 && i < count ? v.getUint16(def + 6 + i * 2) : 0;
845
+ }
846
+ if (format === 2) {
847
+ let lo = 0;
848
+ let hi = v.getUint16(def + 2) - 1;
849
+ while (lo <= hi) {
850
+ const mid = lo + hi >> 1;
851
+ const rec = def + 4 + mid * 6;
852
+ if (glyph < v.getUint16(rec)) hi = mid - 1;
853
+ else if (glyph > v.getUint16(rec + 2)) lo = mid + 1;
854
+ else return v.getUint16(rec + 4);
855
+ }
856
+ }
857
+ return 0;
858
+ }
859
+
603
860
  // src/outline/opentypeParser.ts
604
861
  var modulePromise = null;
605
862
  function loadOpenType() {
@@ -610,11 +867,14 @@ function parserOf(ns) {
610
867
  const interop = ns;
611
868
  return interop.default?.parse ?? ns.parse;
612
869
  }
613
- function faceFor(font) {
870
+ function faceFor(font, gpos) {
614
871
  const upem = font.unitsPerEm;
872
+ const { hhea, post, os2 } = font.tables;
873
+ const faceMetrics = faceMetricsFromTables({ unitsPerEm: upem, hhea, post, os2 });
615
874
  return {
616
875
  unitsPerEm: upem,
617
- ascender: font.ascender / upem,
876
+ ascender: faceMetrics?.ascent ?? font.ascender / upem,
877
+ ...faceMetrics ? { faceMetrics } : {},
618
878
  advanceOf(cp) {
619
879
  const index = font.charToGlyphIndex(String.fromCodePoint(cp));
620
880
  if (!index) return null;
@@ -624,6 +884,7 @@ function faceFor(font) {
624
884
  const l = font.charToGlyphIndex(String.fromCodePoint(left));
625
885
  const r = font.charToGlyphIndex(String.fromCodePoint(right));
626
886
  if (!l || !r) return 0;
887
+ if (gpos) return gpos(l, r) / upem;
627
888
  return font.getKerningValue(font.glyphs.get(l), font.glyphs.get(r)) / upem;
628
889
  },
629
890
  glyphD(cp) {
@@ -638,7 +899,7 @@ function createOpenTypeParser(postScriptName) {
638
899
  return async (bytes) => {
639
900
  const parse = parserOf(await loadOpenType());
640
901
  const { bytes: single } = sfntFromCollection(bytes, postScriptName);
641
- return faceFor(parse(single));
902
+ return faceFor(parse(single), gposPairKerning(single));
642
903
  };
643
904
  }
644
905
  var openTypeParser = createOpenTypeParser();
@@ -659,12 +920,15 @@ function registerFontOutlines(family, variant, source, opts = {}) {
659
920
  weight,
660
921
  style,
661
922
  source,
923
+ cssSrc: opts.cssSrc,
662
924
  parser: opts.parser ?? openTypeParser,
663
925
  status: "idle",
664
926
  face: null,
665
- glyphs: /* @__PURE__ */ new Map()
927
+ glyphs: /* @__PURE__ */ new Map(),
928
+ load: null
666
929
  });
667
930
  notifyGlyphReady();
931
+ if (source instanceof ArrayBuffer) announceBytes(slots.get(slotKey(family, weight, style)), source);
668
932
  }
669
933
  function unregisterFontOutlines(family, variant = {}) {
670
934
  const { weight, style } = normalize(variant);
@@ -728,23 +992,57 @@ function outlineMetrics(family, weight, style) {
728
992
  }
729
993
  return slot.status === "ready" ? slot.face : null;
730
994
  }
731
- async function beginLoad(slot) {
995
+ async function loadFontOutlines(family, variant = {}) {
996
+ const { weight, style } = normalize(variant);
997
+ const slot = slots.get(slotKey(family, weight, style));
998
+ if (!slot) return null;
999
+ await beginLoad(slot);
1000
+ return slot.status;
1001
+ }
1002
+ function beginLoad(slot) {
1003
+ slot.load ??= runLoad(slot);
1004
+ return slot.load;
1005
+ }
1006
+ async function runLoad(slot) {
732
1007
  slot.status = "loading";
733
1008
  slots.bump();
734
1009
  try {
735
- slot.face = await slot.parser(await readSource(slot.source));
1010
+ const bytes = await readSource(slot.source);
1011
+ announceBytes(slot, bytes);
1012
+ slot.face = await slot.parser(bytes);
736
1013
  slot.status = "ready";
737
1014
  slots.bump();
738
1015
  notifyGlyphReady();
739
1016
  } catch (err) {
740
1017
  slot.status = "failed";
741
1018
  slots.bump();
1019
+ notifyGlyphReady();
742
1020
  warnOnce(
743
1021
  `load|${slotKey(slot.family, slot.weight, slot.style)}`,
744
1022
  `weasel registerFontOutlines("${slot.family}" ${slot.weight}/${slot.style}): ${err instanceof Error ? err.message : String(err)}. Large text in this face keeps rendering from the SDF tier.`
745
1023
  );
746
1024
  }
747
1025
  }
1026
+ function outlineCssSource(family, weight, style) {
1027
+ const slot = slots.get(slotKey(family, weight, style));
1028
+ if (!slot) return null;
1029
+ if (slot.cssSrc !== void 0) return { registration: slot, src: slot.cssSrc };
1030
+ if (typeof slot.source === "string") {
1031
+ return { registration: slot, url: `url(${JSON.stringify(slot.source)})` };
1032
+ }
1033
+ const source = slot.source;
1034
+ return { registration: slot, bytes: () => readSource(source) };
1035
+ }
1036
+ var bytesListener = null;
1037
+ function onOutlineBytes(listener) {
1038
+ bytesListener = listener;
1039
+ }
1040
+ function announceBytes(slot, bytes) {
1041
+ try {
1042
+ bytesListener?.(slot, bytes);
1043
+ } catch {
1044
+ }
1045
+ }
748
1046
  async function readSource(source) {
749
1047
  const resolved = typeof source === "function" ? await source() : source;
750
1048
  if (typeof resolved === "string") {
@@ -776,17 +1074,33 @@ function normalizeVariant(v) {
776
1074
  style: v.style ?? "normal"
777
1075
  };
778
1076
  }
1077
+ var inFlight = /* @__PURE__ */ new Map();
1078
+ var loads = /* @__PURE__ */ new Map();
1079
+ var declared = /* @__PURE__ */ new Map();
1080
+ var registeredFamilies = /* @__PURE__ */ new Set();
779
1081
  function _resetFontRegistryForTests() {
780
1082
  registry.clear();
1083
+ inFlight.clear();
1084
+ loads.clear();
1085
+ declared.clear();
1086
+ registeredFamilies.clear();
781
1087
  _clearFallbackWarnings();
782
1088
  }
783
1089
  function getFont(family, weight = 400, style = "normal") {
784
1090
  return registry.get(family)?.get(variantKey(weight, style)) ?? null;
785
1091
  }
1092
+ function faceMetricsFor(family, weight = 400, style = "normal") {
1093
+ return getFont(family, weight, style)?.font.faceMetrics ?? outlineMetrics(family, weight, style === "italic" ? "italic" : "normal")?.faceMetrics;
1094
+ }
786
1095
  function listFonts() {
787
1096
  const out = [];
788
- for (const { key: family, value: variantMap } of registry.entries()) {
789
- const variants = [...variantMap.keys()].map((key) => {
1097
+ for (const family of registeredFamilies) {
1098
+ const keys = /* @__PURE__ */ new Set([
1099
+ ...registry.get(family)?.keys() ?? [],
1100
+ ...declared.get(family)?.keys() ?? []
1101
+ ]);
1102
+ if (keys.size === 0) continue;
1103
+ const variants = [...keys].map((key) => {
790
1104
  const [w, s] = key.split("|");
791
1105
  return { weight: Number(w), style: s };
792
1106
  }).sort((a, b) => a.weight - b.weight || a.style.localeCompare(b.style));
@@ -794,10 +1108,60 @@ function listFonts() {
794
1108
  }
795
1109
  return out;
796
1110
  }
797
- async function registerFont(family, variant, metricsUrl, atlasUrl) {
1111
+ function listFontWeights(family) {
1112
+ const weights = /* @__PURE__ */ new Set();
1113
+ for (const key of registry.get(family)?.keys() ?? []) weights.add(Number(key.split("|")[0]));
1114
+ for (const key of declared.get(family)?.keys() ?? []) weights.add(Number(key.split("|")[0]));
1115
+ for (const face of listFontOutlines()) {
1116
+ if (face.family === family && face.status !== "failed") weights.add(face.weight);
1117
+ }
1118
+ return [...weights].sort((a, b) => a - b);
1119
+ }
1120
+ function registerFont(family, variant, metricsUrl, atlasUrl, opts = {}) {
798
1121
  const { weight, style } = normalizeVariant(variant);
799
1122
  const key = variantKey(weight, style);
800
- if (registry.get(family)?.has(key)) return;
1123
+ registeredFamilies.add(family);
1124
+ if (registry.get(family)?.has(key)) return Promise.resolve();
1125
+ if (!opts.lazy) return loadVariant(family, key, metricsUrl, atlasUrl);
1126
+ const running = loads.get(`${family}|${key}`);
1127
+ if (running) return running;
1128
+ const existing = declared.get(family)?.get(key);
1129
+ if (existing) return existing.promise;
1130
+ let settle2;
1131
+ const promise = new Promise((resolve, reject) => {
1132
+ settle2 = (load) => {
1133
+ load.then(resolve, reject);
1134
+ };
1135
+ });
1136
+ const variants = declared.get(family) ?? /* @__PURE__ */ new Map();
1137
+ declared.set(family, variants.set(key, { metricsUrl, atlasUrl, promise, settle: settle2 }));
1138
+ notifyGlyphReady();
1139
+ return promise;
1140
+ }
1141
+ function loadVariant(family, key, metricsUrl, atlasUrl) {
1142
+ const id = `${family}|${key}`;
1143
+ const running = loads.get(id);
1144
+ if (running) return running;
1145
+ const load = fetchVariant(family, key, metricsUrl, atlasUrl);
1146
+ loads.set(id, load);
1147
+ const settleLoad = () => {
1148
+ if (loads.get(id) === load) loads.delete(id);
1149
+ };
1150
+ load.then(settleLoad, settleLoad);
1151
+ const pendingLazy = declared.get(family)?.get(key);
1152
+ if (pendingLazy) {
1153
+ const variants = declared.get(family);
1154
+ variants.delete(key);
1155
+ if (variants.size === 0) declared.delete(family);
1156
+ pendingLazy.settle(load);
1157
+ }
1158
+ return load;
1159
+ }
1160
+ async function fetchVariant(family, key, metricsUrl, atlasUrl) {
1161
+ const [weight, style] = key.split("|");
1162
+ const token = {};
1163
+ const tokens = inFlight.get(family) ?? /* @__PURE__ */ new Set();
1164
+ inFlight.set(family, tokens.add(token));
801
1165
  try {
802
1166
  const [metricsRes, atlasRes] = await Promise.all([
803
1167
  fetch(metricsUrl),
@@ -816,13 +1180,75 @@ async function registerFont(family, variant, metricsUrl, atlasUrl) {
816
1180
  const font = parseBmFont(rawJson);
817
1181
  const bitmap = await createImageBitmap(blob);
818
1182
  registry.set(family, new Map(registry.get(family)).set(key, { font, bitmap }));
1183
+ settle(family, token);
819
1184
  notifyGlyphReady();
820
1185
  } catch (err) {
1186
+ settle(family, token);
1187
+ notifyGlyphReady();
821
1188
  throw new Error(
822
1189
  `weasel registerFont("${family}" ${weight}/${style}): ${err instanceof Error ? err.message : String(err)}`
823
1190
  );
824
1191
  }
825
1192
  }
1193
+ function wakeDeclared(family, weight, style) {
1194
+ const variants = declared.get(family);
1195
+ if (!variants) return;
1196
+ const exact = variantKey(weight, style);
1197
+ const keys = variants.has(exact) ? [exact] : [...variants.keys()];
1198
+ for (const key of keys) {
1199
+ const { metricsUrl, atlasUrl } = variants.get(key);
1200
+ loadVariant(family, key, metricsUrl, atlasUrl).catch(() => {
1201
+ });
1202
+ }
1203
+ }
1204
+ function warmFonts(fonts) {
1205
+ const pending2 = [];
1206
+ for (const font of fonts ?? [...registeredFamilies]) {
1207
+ if (typeof font !== "string") {
1208
+ const { weight, style } = normalizeVariant(font);
1209
+ pending2.push(...variantLoads(font.family, weight, style));
1210
+ continue;
1211
+ }
1212
+ if (!registeredFamilies.has(font)) {
1213
+ return Promise.reject(new Error(`weasel warmFonts: "${font}" was never registered.`));
1214
+ }
1215
+ for (const [key, { metricsUrl, atlasUrl }] of [...declared.get(font) ?? []]) {
1216
+ loadVariant(font, key, metricsUrl, atlasUrl);
1217
+ }
1218
+ pending2.push(...familyLoads(font));
1219
+ }
1220
+ return Promise.all(pending2).then(() => void 0);
1221
+ }
1222
+ function familyLoads(family) {
1223
+ return [...loads].filter(([id]) => id.startsWith(`${family}|`)).map(([, load]) => load);
1224
+ }
1225
+ function variantLoads(family, weight, style) {
1226
+ const out = [];
1227
+ const outlines = hasFontOutlines(family, weight, style);
1228
+ if (outlines) out.push(loadFontOutlines(family, { weight, style }));
1229
+ const key = variantKey(weight, style);
1230
+ if (registry.get(family)?.has(key)) return out;
1231
+ wakeDeclared(family, weight, style);
1232
+ const exact = loads.get(`${family}|${key}`);
1233
+ if (exact) return [...out, exact];
1234
+ if (registry.has(family) || atlasPending(family)) return [...out, ...familyLoads(family)];
1235
+ if (outlines || isExplicitCanvasFont(family) || getFontFallbackPolicy() !== "substitute") return out;
1236
+ const fallback = getDefaultFontFamily() ?? firstRegisteredFamily();
1237
+ return fallback !== null && fallback !== family ? [...out, ...variantLoads(fallback, weight, style)] : out;
1238
+ }
1239
+ function atlasPending(family) {
1240
+ return inFlight.has(family) || declared.has(family);
1241
+ }
1242
+ function settle(family, token) {
1243
+ const tokens = inFlight.get(family);
1244
+ if (!tokens?.delete(token)) return;
1245
+ if (tokens.size === 0) inFlight.delete(family);
1246
+ }
1247
+ function fontPending(family, weight = 400, style = "normal") {
1248
+ if (atlasPending(family)) return true;
1249
+ const outline = outlineStatus(family, weight, style === "italic" ? "italic" : "normal");
1250
+ return outline === "idle" || outline === "loading";
1251
+ }
826
1252
  function ensureFontTexture(family, weight, style, textureCache) {
827
1253
  const entry = getFont(family, weight, style);
828
1254
  if (!entry) return false;
@@ -868,12 +1294,13 @@ function missResolveResult(family, weight, style, suppressWarn = false) {
868
1294
  if (policy2 === "substitute") {
869
1295
  const fallback = getDefaultFontFamily() ?? firstRegisteredFamily();
870
1296
  if (fallback !== null && fallback !== family) {
871
- if (registry.has(fallback) || isCanvasFont(fallback)) {
1297
+ if (registry.has(fallback) || isCanvasFont(fallback) || atlasPending(fallback)) {
872
1298
  const result = resolveFontVariantInternal(fallback, weight, style, true);
873
1299
  if (result.entry !== null || result.dynamicFace !== void 0) {
874
1300
  if (!suppressWarn) warnMissingFamilyOnce(family, weight, style, fallback);
875
1301
  return { ...result, substituted: { requested: family, resolved: fallback } };
876
1302
  }
1303
+ if (atlasPending(fallback)) return pendingMiss(family, weight, style);
877
1304
  }
878
1305
  if (!suppressWarn) warnUnusableDefaultOnce(family, weight, style, fallback);
879
1306
  } else if (fallback === family && !suppressWarn) {
@@ -892,7 +1319,18 @@ function missResolveResult(family, weight, style, suppressWarn = false) {
892
1319
  };
893
1320
  }
894
1321
  function firstRegisteredFamily() {
895
- return registry.entries()[0]?.key ?? null;
1322
+ for (const family of registeredFamilies) {
1323
+ if (registry.has(family) || atlasPending(family)) return family;
1324
+ }
1325
+ return null;
1326
+ }
1327
+ function pendingMiss(family, weight, style) {
1328
+ return {
1329
+ entry: null,
1330
+ resolved: { family, weight, style },
1331
+ synthetic: { bold: false, italic: false },
1332
+ source: "atlas"
1333
+ };
896
1334
  }
897
1335
  function warnMissingFamilyOnce(family, weight, style, resolved) {
898
1336
  if (!claimFallbackWarning(`substituted|${family}|${weight}|${style}`)) return;
@@ -945,11 +1383,17 @@ function resolveGlyphFallback(family, weight, style) {
945
1383
  }
946
1384
  }
947
1385
  function resolveFontVariantInternal(family, weight, style, suppressWarn) {
1386
+ wakeDeclared(family, weight, style);
1387
+ const exactKey = variantKey(weight, style);
1388
+ if (loads.has(`${family}|${exactKey}`) && !registry.get(family)?.has(exactKey)) {
1389
+ return pendingMiss(family, weight, style);
1390
+ }
948
1391
  const familyMap = registry.get(family);
949
1392
  if (!familyMap || familyMap.size === 0) {
1393
+ if (atlasPending(family)) return pendingMiss(family, weight, style);
950
1394
  return missResolveResult(family, weight, style, suppressWarn);
951
1395
  }
952
- const exact = familyMap.get(variantKey(weight, style));
1396
+ const exact = familyMap.get(exactKey);
953
1397
  if (exact) {
954
1398
  return {
955
1399
  entry: exact,
@@ -1039,6 +1483,107 @@ function resolveFontVariantInternal(family, weight, style, suppressWarn) {
1039
1483
  return missResolveResult(family, weight, style, suppressWarn);
1040
1484
  }
1041
1485
 
1042
- export { DEFAULT_BAKE_BUDGET, FIXTURE_FONT, __setGlyphRasterizerForTests, _getPagesForTests, _resetDynamicFontsForTests, _resetFallbackForTests, _resetFontOutlinesForTests, _resetFontRegistryForTests, createOpenTypeParser, dynamicPageTextureId, ensureFontTexture, fontOutlineRegistry, fontRegistry, getDefaultFontFamily, getFont, getFontFallbackPolicy, glyphGeneration, glyphOutline, hasFontOutlines, isCanvasFont, listCanvasFonts, listFontOutlines, listFonts, markAllFontsNotUploaded, outlineMetrics, outlineStatus, parseBmFont, registerCanvasFont, registerFont, registerFontOutlines, resetBakeBudget, resolveFontVariant, resolveGlyphFallback, setDefaultFontFamily, setFontFallbackPolicy, subscribeGlyphReady, syncDynamicPageTexture, textureCacheKey, unregisterCanvasFont, unregisterFontOutlines };
1043
- //# sourceMappingURL=chunk-CDPTPB26.js.map
1044
- //# sourceMappingURL=chunk-CDPTPB26.js.map
1486
+ // src/domFace.ts
1487
+ var names = /* @__PURE__ */ new Map();
1488
+ var faces2 = /* @__PURE__ */ new Map();
1489
+ var warned2 = /* @__PURE__ */ new Set();
1490
+ function domFonts() {
1491
+ return typeof FontFace !== "undefined" && typeof document !== "undefined" && !!document.fonts;
1492
+ }
1493
+ function nameFor(family) {
1494
+ let name = names.get(family);
1495
+ if (!name) {
1496
+ name = `weasel-face-${names.size}`;
1497
+ names.set(family, name);
1498
+ }
1499
+ return name;
1500
+ }
1501
+ onOutlineBytes((registration, bytes) => {
1502
+ if (!domFonts()) return;
1503
+ const { family, weight, style } = registration;
1504
+ ensureFace(nameFor(family), family, weight, style, bytes);
1505
+ });
1506
+ function privateFamily(family, variant) {
1507
+ if (!domFonts()) return null;
1508
+ const resolved = resolveFontVariant(family, variant.weight ?? 400, variant.style ?? "normal");
1509
+ if (resolved.source === "canvas") return null;
1510
+ const target = resolved.resolved.family;
1511
+ const variants = listFontOutlines().filter((f) => f.family === target && f.status !== "failed");
1512
+ if (variants.length === 0) {
1513
+ if (resolved.entry && !warned2.has(target)) {
1514
+ warned2.add(target);
1515
+ console.warn(
1516
+ `weasel: "${target}" draws from a baked atlas with no font file registered, so DOM text in it (the text edit overlay) uses the browser's own "${family}" and will not match the canvas. Register the file the atlas was baked from: registerFontOutlines("${target}", \u2026).`
1517
+ );
1518
+ }
1519
+ return null;
1520
+ }
1521
+ return { target, variants };
1522
+ }
1523
+ function cssFontFamily(family, variant = {}) {
1524
+ const priv = privateFamily(family, variant);
1525
+ if (!priv) return family;
1526
+ const name = nameFor(priv.target);
1527
+ for (const v of priv.variants) ensureFace(name, v.family, v.weight, v.style);
1528
+ return `${JSON.stringify(name)}, ${family}`;
1529
+ }
1530
+ function cssFontFamilyLoading(family, variant = {}) {
1531
+ const priv = privateFamily(family, variant);
1532
+ if (!priv) return null;
1533
+ const name = nameFor(priv.target);
1534
+ for (const v of priv.variants) ensureFace(name, v.family, v.weight, v.style);
1535
+ const weight = variant.weight ?? 400;
1536
+ const style = variant.style ?? "normal";
1537
+ const exact = priv.variants.filter((v) => v.weight === weight && v.style === style);
1538
+ const pending2 = (exact.length > 0 ? exact : priv.variants).map((v) => faces2.get(`${v.family}|${v.weight}|${v.style}`)).filter((l) => l !== void 0 && !l.settled).map((l) => l.done);
1539
+ return pending2.length === 0 ? null : Promise.all(pending2).then(() => {
1540
+ });
1541
+ }
1542
+ function ensureFace(name, family, weight, style, bytes) {
1543
+ const key = `${family}|${weight}|${style}`;
1544
+ const css = outlineCssSource(family, weight, style);
1545
+ if (!css) return;
1546
+ const have = faces2.get(key);
1547
+ if (have?.from === css.registration) return;
1548
+ if (have?.face) document.fonts.delete(have.face);
1549
+ let settle2;
1550
+ const entry = {
1551
+ from: css.registration,
1552
+ face: null,
1553
+ settled: false,
1554
+ done: new Promise((r) => {
1555
+ settle2 = r;
1556
+ })
1557
+ };
1558
+ const finish = () => {
1559
+ entry.settled = true;
1560
+ settle2();
1561
+ };
1562
+ faces2.set(key, entry);
1563
+ const descriptors = { weight: String(weight), style, display: "swap" };
1564
+ const add = (face) => {
1565
+ if (faces2.get(key) !== entry) return finish();
1566
+ entry.face = face;
1567
+ document.fonts.add(face);
1568
+ face.load().then(finish, () => {
1569
+ finish();
1570
+ console.warn(`weasel: could not load "${family}" (${weight}/${style}) for DOM text; the text edit overlay falls back to the browser's own face.`);
1571
+ });
1572
+ };
1573
+ if ("src" in css) add(new FontFace(name, css.src, descriptors));
1574
+ else if (bytes !== void 0) add(new FontFace(name, bytes, descriptors));
1575
+ else if ("url" in css) add(new FontFace(name, css.url, descriptors));
1576
+ else css.bytes().then((buf) => add(new FontFace(name, buf, descriptors)), finish);
1577
+ }
1578
+ function _resetDomFacesForTests() {
1579
+ if (typeof document !== "undefined" && document.fonts) {
1580
+ for (const { face } of faces2.values()) if (face) document.fonts.delete(face);
1581
+ }
1582
+ names.clear();
1583
+ faces2.clear();
1584
+ warned2.clear();
1585
+ }
1586
+
1587
+ export { DEFAULT_BAKE_BUDGET, FIXTURE_FONT, PAGE_SIZE, SDF_RADIUS, __setGlyphRasterizerForTests, _getPagesForTests, _resetDomFacesForTests, _resetDynamicFontsForTests, _resetFallbackForTests, _resetFontOutlinesForTests, _resetFontRegistryForTests, createOpenTypeParser, cssFontFamily, cssFontFamilyLoading, dynamicPageTextureId, ensureFontTexture, faceMetricsFor, faceMetricsFromTables, faceMetricsOf, fontOutlineRegistry, fontPending, fontRegistry, getDefaultFontFamily, getFont, getFontFallbackPolicy, glyphGeneration, glyphOutline, hasFontOutlines, isCanvasFont, listCanvasFonts, listFontOutlines, listFontWeights, listFonts, loadFontOutlines, markAllFontsNotUploaded, outlineMetrics, outlineStatus, parseBmFont, registerCanvasFont, registerFont, registerFontOutlines, resetBakeBudget, resolveFontVariant, resolveGlyphFallback, setDefaultFontFamily, setFontFallbackPolicy, subscribeGlyphReady, syncDynamicPageTexture, textureCacheKey, unregisterCanvasFont, unregisterFontOutlines, verticalMetricsFromTables, warmFonts };
1588
+ //# sourceMappingURL=chunk-RYN4NYX4.js.map
1589
+ //# sourceMappingURL=chunk-RYN4NYX4.js.map