@weasel-js/font 1.6.1 → 1.7.1

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,3 +1,82 @@
1
+ import { createReflectable } from '@weasel-js/registry';
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
+
1
80
  // src/FontAtlas.ts
2
81
  var FIXTURE_FONT = {
3
82
  info: { face: "Inter", size: 32 },
@@ -31,7 +110,18 @@ function parseBmFont(raw) {
31
110
  }
32
111
  inner.set(k.second, k.amount);
33
112
  }
34
- 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
+ };
35
125
  }
36
126
 
37
127
  // src/glyphReady.ts
@@ -58,13 +148,17 @@ function _clearGlyphReadySubscribers() {
58
148
  var policy = "substitute";
59
149
  var defaultFamily = null;
60
150
  function setFontFallbackPolicy(next) {
151
+ if (next === policy) return;
61
152
  policy = next;
153
+ notifyGlyphReady();
62
154
  }
63
155
  function getFontFallbackPolicy() {
64
156
  return policy;
65
157
  }
66
158
  function setDefaultFontFamily(family) {
159
+ if (family === defaultFamily) return;
67
160
  defaultFamily = family;
161
+ notifyGlyphReady();
68
162
  }
69
163
  function getDefaultFontFamily() {
70
164
  return defaultFamily;
@@ -203,8 +297,9 @@ function alphaToSdf(alpha, width, height, radius, cutoff) {
203
297
  // src/dynamic/glyphRasterizer.ts
204
298
  var BAKE_SIZE = 48;
205
299
  var PAD = 8;
206
- function cssFontString(weight, style, family) {
207
- 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)}`;
208
303
  }
209
304
  function createCanvasRasterizer() {
210
305
  let canvas;
@@ -219,18 +314,19 @@ function createCanvasRasterizer() {
219
314
  }
220
315
  const ctx = canvas.getContext("2d", { willReadFrequently: true });
221
316
  if (!ctx) throw new Error("weasel DynamicGlyphAtlas: 2D context unavailable");
222
- function setFont(family, weight, style) {
223
- ctx.font = cssFontString(weight, style, family);
317
+ function setFont(family, weight, style, size = BAKE_SIZE) {
318
+ ctx.font = cssFontString(weight, style, family, size);
224
319
  ctx.textBaseline = "alphabetic";
225
320
  ctx.textAlign = "left";
226
321
  }
227
322
  return {
228
323
  faceMetrics(family, weight, style) {
229
- setFont(family, weight, style);
324
+ setFont(family, weight, style, METRICS_SIZE);
230
325
  const m = ctx.measureText("Hg");
326
+ const k = BAKE_SIZE / METRICS_SIZE;
231
327
  return {
232
- ascent: m.fontBoundingBoxAscent ?? BAKE_SIZE * 0.8,
233
- 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
234
330
  };
235
331
  },
236
332
  rasterize(family, weight, style, codepoint) {
@@ -292,8 +388,10 @@ function getRasterizer() {
292
388
  return rasterizer;
293
389
  }
294
390
  function registerCanvasFont(family) {
391
+ if (isExplicitCanvasFont(family)) return;
295
392
  canvasFamilies.add(family);
296
393
  autoEnrolledFamilies.delete(family);
394
+ notifyGlyphReady();
297
395
  }
298
396
  function isCanvasFont(family) {
299
397
  if (!canvasFamilies.has(family)) return false;
@@ -316,11 +414,13 @@ function isExplicitCanvasFont(family) {
316
414
  return canvasFamilies.has(family) && !autoEnrolledFamilies.has(family);
317
415
  }
318
416
  function unregisterCanvasFont(family) {
417
+ if (!canvasFamilies.has(family)) return;
319
418
  canvasFamilies.delete(family);
320
419
  autoEnrolledFamilies.delete(family);
321
420
  for (const key of [...faces.keys()]) {
322
421
  if (faces.get(key).family === family) faces.delete(key);
323
422
  }
423
+ notifyGlyphReady();
324
424
  }
325
425
  function getDynamicFace(family, weight, style) {
326
426
  const key = `${family}|${weight}|${style}`;
@@ -341,7 +441,9 @@ function getDynamicFace(family, weight, style) {
341
441
  kernings: [],
342
442
  // no kerning in v1 (measured-pair kerning is future work)
343
443
  charMap: /* @__PURE__ */ new Map(),
344
- 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 }
345
447
  };
346
448
  const face = {
347
449
  family,
@@ -598,6 +700,163 @@ function sfntFromCollection(bytes, postScriptName) {
598
700
  return { bytes: extractFont(bytes, view, offsets[0]), matched: !postScriptName };
599
701
  }
600
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
+
601
860
  // src/outline/opentypeParser.ts
602
861
  var modulePromise = null;
603
862
  function loadOpenType() {
@@ -608,11 +867,14 @@ function parserOf(ns) {
608
867
  const interop = ns;
609
868
  return interop.default?.parse ?? ns.parse;
610
869
  }
611
- function faceFor(font) {
870
+ function faceFor(font, gpos) {
612
871
  const upem = font.unitsPerEm;
872
+ const { hhea, post, os2 } = font.tables;
873
+ const faceMetrics = faceMetricsFromTables({ unitsPerEm: upem, hhea, post, os2 });
613
874
  return {
614
875
  unitsPerEm: upem,
615
- ascender: font.ascender / upem,
876
+ ascender: faceMetrics?.ascent ?? font.ascender / upem,
877
+ ...faceMetrics ? { faceMetrics } : {},
616
878
  advanceOf(cp) {
617
879
  const index = font.charToGlyphIndex(String.fromCodePoint(cp));
618
880
  if (!index) return null;
@@ -622,6 +884,7 @@ function faceFor(font) {
622
884
  const l = font.charToGlyphIndex(String.fromCodePoint(left));
623
885
  const r = font.charToGlyphIndex(String.fromCodePoint(right));
624
886
  if (!l || !r) return 0;
887
+ if (gpos) return gpos(l, r) / upem;
625
888
  return font.getKerningValue(font.glyphs.get(l), font.glyphs.get(r)) / upem;
626
889
  },
627
890
  glyphD(cp) {
@@ -636,13 +899,14 @@ function createOpenTypeParser(postScriptName) {
636
899
  return async (bytes) => {
637
900
  const parse = parserOf(await loadOpenType());
638
901
  const { bytes: single } = sfntFromCollection(bytes, postScriptName);
639
- return faceFor(parse(single));
902
+ return faceFor(parse(single), gposPairKerning(single));
640
903
  };
641
904
  }
642
905
  var openTypeParser = createOpenTypeParser();
643
906
 
644
907
  // src/outline/outlineRegistry.ts
645
- var slots = /* @__PURE__ */ new Map();
908
+ var slots = createReflectable();
909
+ var fontOutlineRegistry = slots.reflection;
646
910
  function slotKey(family, weight, style) {
647
911
  return `${family}|${weight}|${style}`;
648
912
  }
@@ -656,12 +920,15 @@ function registerFontOutlines(family, variant, source, opts = {}) {
656
920
  weight,
657
921
  style,
658
922
  source,
923
+ cssSrc: opts.cssSrc,
659
924
  parser: opts.parser ?? openTypeParser,
660
925
  status: "idle",
661
926
  face: null,
662
- glyphs: /* @__PURE__ */ new Map()
927
+ glyphs: /* @__PURE__ */ new Map(),
928
+ load: null
663
929
  });
664
930
  notifyGlyphReady();
931
+ if (source instanceof ArrayBuffer) announceBytes(slots.get(slotKey(family, weight, style)), source);
665
932
  }
666
933
  function unregisterFontOutlines(family, variant = {}) {
667
934
  const { weight, style } = normalize(variant);
@@ -674,7 +941,7 @@ function outlineStatus(family, weight = 400, style = "normal") {
674
941
  return slots.get(slotKey(family, weight, style))?.status ?? null;
675
942
  }
676
943
  function listFontOutlines() {
677
- return [...slots.values()].map(({ family, weight, style, status }) => ({ family, weight, style, status })).sort((a, b) => a.family.localeCompare(b.family) || a.weight - b.weight || a.style.localeCompare(b.style));
944
+ return slots.entries().map(({ value: { family, weight, style, status } }) => ({ family, weight, style, status })).sort((a, b) => a.family.localeCompare(b.family) || a.weight - b.weight || a.style.localeCompare(b.style));
678
945
  }
679
946
  function closeContours(d) {
680
947
  let out = "";
@@ -725,20 +992,57 @@ function outlineMetrics(family, weight, style) {
725
992
  }
726
993
  return slot.status === "ready" ? slot.face : null;
727
994
  }
728
- 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) {
729
1007
  slot.status = "loading";
1008
+ slots.bump();
730
1009
  try {
731
- 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);
732
1013
  slot.status = "ready";
1014
+ slots.bump();
733
1015
  notifyGlyphReady();
734
1016
  } catch (err) {
735
1017
  slot.status = "failed";
1018
+ slots.bump();
1019
+ notifyGlyphReady();
736
1020
  warnOnce(
737
1021
  `load|${slotKey(slot.family, slot.weight, slot.style)}`,
738
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.`
739
1023
  );
740
1024
  }
741
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
+ }
742
1046
  async function readSource(source) {
743
1047
  const resolved = typeof source === "function" ? await source() : source;
744
1048
  if (typeof resolved === "string") {
@@ -756,12 +1060,11 @@ function warnOnce(key, message) {
756
1060
  console.warn(message);
757
1061
  }
758
1062
  function _resetFontOutlinesForTests() {
759
- slots = /* @__PURE__ */ new Map();
1063
+ slots.clear();
760
1064
  warned.clear();
761
1065
  }
762
-
763
- // src/registerFont.ts
764
- var registry = /* @__PURE__ */ new Map();
1066
+ var registry = createReflectable();
1067
+ var fontRegistry = registry.reflection;
765
1068
  function variantKey(weight, style) {
766
1069
  return `${weight}|${style}`;
767
1070
  }
@@ -771,17 +1074,33 @@ function normalizeVariant(v) {
771
1074
  style: v.style ?? "normal"
772
1075
  };
773
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();
774
1081
  function _resetFontRegistryForTests() {
775
- registry = /* @__PURE__ */ new Map();
1082
+ registry.clear();
1083
+ inFlight.clear();
1084
+ loads.clear();
1085
+ declared.clear();
1086
+ registeredFamilies.clear();
776
1087
  _clearFallbackWarnings();
777
1088
  }
778
1089
  function getFont(family, weight = 400, style = "normal") {
779
1090
  return registry.get(family)?.get(variantKey(weight, style)) ?? null;
780
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
+ }
781
1095
  function listFonts() {
782
1096
  const out = [];
783
- for (const [family, variantMap] of registry) {
784
- 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) => {
785
1104
  const [w, s] = key.split("|");
786
1105
  return { weight: Number(w), style: s };
787
1106
  }).sort((a, b) => a.weight - b.weight || a.style.localeCompare(b.style));
@@ -789,10 +1108,60 @@ function listFonts() {
789
1108
  }
790
1109
  return out;
791
1110
  }
792
- 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 = {}) {
793
1121
  const { weight, style } = normalizeVariant(variant);
794
1122
  const key = variantKey(weight, style);
795
- 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));
796
1165
  try {
797
1166
  const [metricsRes, atlasRes] = await Promise.all([
798
1167
  fetch(metricsUrl),
@@ -810,19 +1179,76 @@ async function registerFont(family, variant, metricsUrl, atlasUrl) {
810
1179
  ]);
811
1180
  const font = parseBmFont(rawJson);
812
1181
  const bitmap = await createImageBitmap(blob);
813
- let familyMap = registry.get(family);
814
- if (!familyMap) {
815
- familyMap = /* @__PURE__ */ new Map();
816
- registry.set(family, familyMap);
817
- }
818
- familyMap.set(key, { font, bitmap });
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,9 +1319,19 @@ function missResolveResult(family, weight, style, suppressWarn = false) {
892
1319
  };
893
1320
  }
894
1321
  function firstRegisteredFamily() {
895
- for (const family of registry.keys()) return family;
1322
+ for (const family of registeredFamilies) {
1323
+ if (registry.has(family) || atlasPending(family)) return family;
1324
+ }
896
1325
  return null;
897
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
+ };
1334
+ }
898
1335
  function warnMissingFamilyOnce(family, weight, style, resolved) {
899
1336
  if (!claimFallbackWarning(`substituted|${family}|${weight}|${style}`)) return;
900
1337
  const cause = registry.has(family) ? `has no variant matching ${weight}/${style}, and none of its registered variants are close enough for the within-family chain to substitute \u2014 rendering with "${resolved}" instead. Bake that variant with registerFont("${family}", { weight: ${weight}, style: '${style}' }, \u2026)` : `is not registered \u2014 rendering with "${resolved}" instead. Call registerFont("${family}", \u2026)`;
@@ -946,11 +1383,17 @@ function resolveGlyphFallback(family, weight, style) {
946
1383
  }
947
1384
  }
948
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
+ }
949
1391
  const familyMap = registry.get(family);
950
1392
  if (!familyMap || familyMap.size === 0) {
1393
+ if (atlasPending(family)) return pendingMiss(family, weight, style);
951
1394
  return missResolveResult(family, weight, style, suppressWarn);
952
1395
  }
953
- const exact = familyMap.get(variantKey(weight, style));
1396
+ const exact = familyMap.get(exactKey);
954
1397
  if (exact) {
955
1398
  return {
956
1399
  entry: exact,
@@ -1040,6 +1483,107 @@ function resolveFontVariantInternal(family, weight, style, suppressWarn) {
1040
1483
  return missResolveResult(family, weight, style, suppressWarn);
1041
1484
  }
1042
1485
 
1043
- export { DEFAULT_BAKE_BUDGET, FIXTURE_FONT, __setGlyphRasterizerForTests, _getPagesForTests, _resetDynamicFontsForTests, _resetFallbackForTests, _resetFontOutlinesForTests, _resetFontRegistryForTests, createOpenTypeParser, dynamicPageTextureId, ensureFontTexture, 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 };
1044
- //# sourceMappingURL=chunk-QDTJ3CPE.js.map
1045
- //# sourceMappingURL=chunk-QDTJ3CPE.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