@tldraw/editor 5.2.0-canary.2e7bbb68abd7 → 5.2.0-canary.2f9a2114decd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +28 -4
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +3 -3
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +28 -4
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +3 -3
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +10 -7
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -1
- package/src/lib/editor/managers/FontManager/FontManager.ts +46 -1
- package/src/version.ts +3 -3
package/dist-cjs/index.js
CHANGED
|
@@ -380,7 +380,7 @@ var import_uniq = require("./lib/utils/uniq");
|
|
|
380
380
|
var import_defaultThemes2 = require("./lib/editor/managers/ThemeManager/defaultThemes");
|
|
381
381
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
382
382
|
"@tldraw/editor",
|
|
383
|
-
"5.2.0-canary.
|
|
383
|
+
"5.2.0-canary.2f9a2114decd",
|
|
384
384
|
"cjs"
|
|
385
385
|
);
|
|
386
386
|
//# sourceMappingURL=index.js.map
|
|
@@ -18,7 +18,8 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var FontManager_exports = {};
|
|
20
20
|
__export(FontManager_exports, {
|
|
21
|
-
FontManager: () => FontManager
|
|
21
|
+
FontManager: () => FontManager,
|
|
22
|
+
clearFontFaceCacheForTests: () => clearFontFaceCacheForTests
|
|
22
23
|
});
|
|
23
24
|
module.exports = __toCommonJS(FontManager_exports);
|
|
24
25
|
var import_state = require("@tldraw/state");
|
|
@@ -127,20 +128,31 @@ class FontManager {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
findOrCreateFontFace(font) {
|
|
130
|
-
const
|
|
131
|
+
const containerDocument = this.editor.getContainerDocument();
|
|
132
|
+
let cache = fontFaceCacheByDocument.get(containerDocument);
|
|
133
|
+
if (!cache) {
|
|
134
|
+
cache = /* @__PURE__ */ new Map();
|
|
135
|
+
fontFaceCacheByDocument.set(containerDocument, cache);
|
|
136
|
+
}
|
|
137
|
+
const key = getFontFaceCacheKey(font);
|
|
138
|
+
const cached = cache.get(key);
|
|
139
|
+
if (cached) return cached;
|
|
140
|
+
const fonts = containerDocument.fonts;
|
|
131
141
|
for (const existing of fonts) {
|
|
132
142
|
if (existing.family === font.family && (0, import_utils.objectMapEntries)(defaultFontFaceDescriptors).every(
|
|
133
|
-
([
|
|
143
|
+
([key2, defaultValue]) => existing[key2] === (font[key2] ?? defaultValue)
|
|
134
144
|
)) {
|
|
145
|
+
cache.set(key, existing);
|
|
135
146
|
return existing;
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
149
|
const url = this.assetUrls?.[font.src.url] ?? font.src.url;
|
|
139
150
|
const instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {
|
|
140
|
-
...(0, import_utils.mapObjectMapValues)(defaultFontFaceDescriptors, (
|
|
151
|
+
...(0, import_utils.mapObjectMapValues)(defaultFontFaceDescriptors, (key2) => font[key2]),
|
|
141
152
|
display: "swap"
|
|
142
153
|
});
|
|
143
154
|
fonts.add(instance);
|
|
155
|
+
cache.set(key, instance);
|
|
144
156
|
return instance;
|
|
145
157
|
}
|
|
146
158
|
async toEmbeddedCssDeclaration(font) {
|
|
@@ -177,4 +189,16 @@ const defaultFontFaceDescriptors = {
|
|
|
177
189
|
descentOverride: "normal",
|
|
178
190
|
lineGapOverride: "normal"
|
|
179
191
|
};
|
|
192
|
+
let fontFaceCacheByDocument = /* @__PURE__ */ new WeakMap();
|
|
193
|
+
function getFontFaceCacheKey(font) {
|
|
194
|
+
return JSON.stringify([
|
|
195
|
+
font.family,
|
|
196
|
+
...(0, import_utils.objectMapEntries)(defaultFontFaceDescriptors).map(
|
|
197
|
+
([key, defaultValue]) => font[key] ?? defaultValue
|
|
198
|
+
)
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
function clearFontFaceCacheForTests() {
|
|
202
|
+
fontFaceCacheByDocument = /* @__PURE__ */ new WeakMap();
|
|
203
|
+
}
|
|
180
204
|
//# sourceMappingURL=FontManager.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/lib/editor/managers/FontManager/FontManager.ts"],
|
|
4
|
-
"sourcesContent": ["import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'\nimport { AtomMap } from '@tldraw/store'\nimport { TLFontFace, TLShape, TLShapeId } from '@tldraw/tlschema'\nimport {\n\tareArraysShallowEqual,\n\tcompact,\n\tFileHelpers,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n} from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\n\ninterface FontState {\n\treadonly state: 'loading' | 'ready' | 'error'\n\treadonly instance: FontFace\n\treadonly loadingPromise: Promise<void>\n}\n\ninterface ShapeFontFacesCache {\n\tget(id: TLShapeId): TLFontFace[] | undefined\n}\n\ninterface ShapeFontLoadStateCache {\n\tget(id: TLShapeId): (FontState | null)[] | undefined\n}\n\nconst EMPTY_SHAPE_FONT_FACES_CACHE: ShapeFontFacesCache = { get: () => undefined }\nconst EMPTY_SHAPE_FONT_LOAD_STATE_CACHE: ShapeFontLoadStateCache = { get: () => undefined }\n\n/** @public */\nexport class FontManager {\n\tconstructor(\n\t\tprivate readonly editor: Editor,\n\t\tprivate readonly assetUrls?: { [key: string]: string | undefined }\n\t) {\n\t\tthis.shapeFontFacesCache = editor.store.createComputedCache(\n\t\t\t'shape font faces',\n\t\t\t(shape: TLShape) => {\n\t\t\t\tconst shapeUtil = this.editor.getShapeUtil(shape)\n\t\t\t\treturn shapeUtil.getFontFaces(shape)\n\t\t\t},\n\t\t\t{\n\t\t\t\tareResultsEqual: areArraysShallowEqual,\n\t\t\t\tareRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,\n\t\t\t}\n\t\t)\n\n\t\tthis.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(\n\t\t\t(id: TLShapeId) => {\n\t\t\t\tconst fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))\n\t\t\t\treturn computed(\n\t\t\t\t\t'font load state',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst states = fontFacesComputed.get().map((face) => this.getFontState(face))\n\t\t\t\t\t\treturn states\n\t\t\t\t\t},\n\t\t\t\t\t{ isEqual: areArraysShallowEqual }\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n\n\tdispose() {\n\t\tthis.fontStates.clear()\n\t\tthis.fontsToLoad.clear()\n\t\tthis.shapeFontFacesCache = EMPTY_SHAPE_FONT_FACES_CACHE\n\t\tthis.shapeFontLoadStateCache = EMPTY_SHAPE_FONT_LOAD_STATE_CACHE\n\t}\n\n\tprivate shapeFontFacesCache: ShapeFontFacesCache\n\tprivate shapeFontLoadStateCache: ShapeFontLoadStateCache\n\n\tgetShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\treturn this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY\n\t}\n\n\ttrackFontsForShape(shape: TLShape | TLShapeId) {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\tthis.shapeFontLoadStateCache.get(shapeId)\n\t}\n\n\tasync loadRequiredFontsForCurrentPage(limit = Infinity) {\n\t\tconst neededFonts = new Set<TLFontFace>()\n\t\tfor (const shapeId of this.editor.getCurrentPageShapeIds()) {\n\t\t\tfor (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {\n\t\t\t\tneededFonts.add(font)\n\t\t\t}\n\t\t}\n\n\t\tif (neededFonts.size > limit) {\n\t\t\treturn\n\t\t}\n\n\t\tconst promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))\n\t\tawait Promise.all(promises)\n\t}\n\n\tprivate readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')\n\tprivate getFontState(font: TLFontFace): FontState | null {\n\t\treturn this.fontStates.get(font) ?? null\n\t}\n\n\tensureFontIsLoaded(font: TLFontFace): Promise<void> {\n\t\tconst existingState = this.getFontState(font)\n\t\tif (existingState) return existingState.loadingPromise\n\n\t\tconst instance = this.findOrCreateFontFace(font)\n\t\tconst state: FontState = {\n\t\t\tstate: 'loading',\n\t\t\tinstance,\n\t\t\tloadingPromise: instance\n\t\t\t\t.load()\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.editor.getContainerDocument().fonts.add(instance)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(err)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'error' }))\n\t\t\t\t}),\n\t\t}\n\n\t\tthis.fontStates.set(font, state)\n\t\treturn state.loadingPromise\n\t}\n\n\tprivate fontsToLoad = new Set<TLFontFace>()\n\trequestFonts(fonts: TLFontFace[]) {\n\t\tif (!this.fontsToLoad.size) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (this.editor.isDisposed) return\n\t\t\t\tconst toLoad = this.fontsToLoad\n\t\t\t\tthis.fontsToLoad = new Set()\n\t\t\t\ttransact(() => {\n\t\t\t\t\tfor (const font of toLoad) {\n\t\t\t\t\t\tthis.ensureFontIsLoaded(font)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t\tfor (const font of fonts) {\n\t\t\tthis.fontsToLoad.add(font)\n\t\t}\n\t}\n\n\tprivate findOrCreateFontFace(font: TLFontFace) {\n\t\tconst
|
|
5
|
-
"mappings": "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
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'\nimport { AtomMap } from '@tldraw/store'\nimport { TLFontFace, TLShape, TLShapeId } from '@tldraw/tlschema'\nimport {\n\tareArraysShallowEqual,\n\tcompact,\n\tFileHelpers,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n} from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\n\ninterface FontState {\n\treadonly state: 'loading' | 'ready' | 'error'\n\treadonly instance: FontFace\n\treadonly loadingPromise: Promise<void>\n}\n\ninterface ShapeFontFacesCache {\n\tget(id: TLShapeId): TLFontFace[] | undefined\n}\n\ninterface ShapeFontLoadStateCache {\n\tget(id: TLShapeId): (FontState | null)[] | undefined\n}\n\nconst EMPTY_SHAPE_FONT_FACES_CACHE: ShapeFontFacesCache = { get: () => undefined }\nconst EMPTY_SHAPE_FONT_LOAD_STATE_CACHE: ShapeFontLoadStateCache = { get: () => undefined }\n\n/** @public */\nexport class FontManager {\n\tconstructor(\n\t\tprivate readonly editor: Editor,\n\t\tprivate readonly assetUrls?: { [key: string]: string | undefined }\n\t) {\n\t\tthis.shapeFontFacesCache = editor.store.createComputedCache(\n\t\t\t'shape font faces',\n\t\t\t(shape: TLShape) => {\n\t\t\t\tconst shapeUtil = this.editor.getShapeUtil(shape)\n\t\t\t\treturn shapeUtil.getFontFaces(shape)\n\t\t\t},\n\t\t\t{\n\t\t\t\tareResultsEqual: areArraysShallowEqual,\n\t\t\t\tareRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,\n\t\t\t}\n\t\t)\n\n\t\tthis.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(\n\t\t\t(id: TLShapeId) => {\n\t\t\t\tconst fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))\n\t\t\t\treturn computed(\n\t\t\t\t\t'font load state',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst states = fontFacesComputed.get().map((face) => this.getFontState(face))\n\t\t\t\t\t\treturn states\n\t\t\t\t\t},\n\t\t\t\t\t{ isEqual: areArraysShallowEqual }\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n\n\tdispose() {\n\t\tthis.fontStates.clear()\n\t\tthis.fontsToLoad.clear()\n\t\tthis.shapeFontFacesCache = EMPTY_SHAPE_FONT_FACES_CACHE\n\t\tthis.shapeFontLoadStateCache = EMPTY_SHAPE_FONT_LOAD_STATE_CACHE\n\t}\n\n\tprivate shapeFontFacesCache: ShapeFontFacesCache\n\tprivate shapeFontLoadStateCache: ShapeFontLoadStateCache\n\n\tgetShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\treturn this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY\n\t}\n\n\ttrackFontsForShape(shape: TLShape | TLShapeId) {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\tthis.shapeFontLoadStateCache.get(shapeId)\n\t}\n\n\tasync loadRequiredFontsForCurrentPage(limit = Infinity) {\n\t\tconst neededFonts = new Set<TLFontFace>()\n\t\tfor (const shapeId of this.editor.getCurrentPageShapeIds()) {\n\t\t\tfor (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {\n\t\t\t\tneededFonts.add(font)\n\t\t\t}\n\t\t}\n\n\t\tif (neededFonts.size > limit) {\n\t\t\treturn\n\t\t}\n\n\t\tconst promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))\n\t\tawait Promise.all(promises)\n\t}\n\n\tprivate readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')\n\tprivate getFontState(font: TLFontFace): FontState | null {\n\t\treturn this.fontStates.get(font) ?? null\n\t}\n\n\tensureFontIsLoaded(font: TLFontFace): Promise<void> {\n\t\tconst existingState = this.getFontState(font)\n\t\tif (existingState) return existingState.loadingPromise\n\n\t\tconst instance = this.findOrCreateFontFace(font)\n\t\tconst state: FontState = {\n\t\t\tstate: 'loading',\n\t\t\tinstance,\n\t\t\tloadingPromise: instance\n\t\t\t\t.load()\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.editor.getContainerDocument().fonts.add(instance)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(err)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'error' }))\n\t\t\t\t}),\n\t\t}\n\n\t\tthis.fontStates.set(font, state)\n\t\treturn state.loadingPromise\n\t}\n\n\tprivate fontsToLoad = new Set<TLFontFace>()\n\trequestFonts(fonts: TLFontFace[]) {\n\t\tif (!this.fontsToLoad.size) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (this.editor.isDisposed) return\n\t\t\t\tconst toLoad = this.fontsToLoad\n\t\t\t\tthis.fontsToLoad = new Set()\n\t\t\t\ttransact(() => {\n\t\t\t\t\tfor (const font of toLoad) {\n\t\t\t\t\t\tthis.ensureFontIsLoaded(font)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t\tfor (const font of fonts) {\n\t\t\tthis.fontsToLoad.add(font)\n\t\t}\n\t}\n\n\tprivate findOrCreateFontFace(font: TLFontFace) {\n\t\tconst containerDocument = this.editor.getContainerDocument()\n\n\t\t// `findOrCreateFontFace` runs for every font on every editor mount, and a fresh\n\t\t// editor (e.g. switching documents) gets a fresh FontManager with no memory of the\n\t\t// previous one. The dedup below is an O(n) scan of the document's whole FontFaceSet,\n\t\t// so without a cache that scan re-ran on every mount (measurably expensive on mobile\n\t\t// Safari). Cache the resolved FontFace per document so repeated lookups - and\n\t\t// remounts - are O(1). Keyed per document for cross-window embedding.\n\t\tlet cache = fontFaceCacheByDocument.get(containerDocument)\n\t\tif (!cache) {\n\t\t\tcache = new Map()\n\t\t\tfontFaceCacheByDocument.set(containerDocument, cache)\n\t\t}\n\t\tconst key = getFontFaceCacheKey(font)\n\t\tconst cached = cache.get(key)\n\t\tif (cached) return cached\n\n\t\tconst fonts = containerDocument.fonts\n\t\t// On a cache miss we still scan once, so font faces added outside this manager\n\t\t// (e.g. preloaded fonts) are reused rather than duplicated.\n\t\tfor (const existing of fonts) {\n\t\t\tif (\n\t\t\t\texisting.family === font.family &&\n\t\t\t\tobjectMapEntries(defaultFontFaceDescriptors).every(\n\t\t\t\t\t([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tcache.set(key, existing)\n\t\t\t\treturn existing\n\t\t\t}\n\t\t}\n\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {\n\t\t\t...mapObjectMapValues(defaultFontFaceDescriptors, (key) => font[key]),\n\t\t\tdisplay: 'swap',\n\t\t})\n\n\t\tfonts.add(instance)\n\t\tcache.set(key, instance)\n\n\t\treturn instance\n\t}\n\n\tasync toEmbeddedCssDeclaration(font: TLFontFace) {\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst dataUrl = await FileHelpers.urlToDataUrl(url)\n\n\t\tconst src = compact([\n\t\t\t`url(\"${dataUrl}\")`,\n\t\t\tfont.src.format ? `format(${font.src.format})` : null,\n\t\t\tfont.src.tech ? `tech(${font.src.tech})` : null,\n\t\t]).join(' ')\n\t\treturn compact([\n\t\t\t`@font-face {`,\n\t\t\t` font-family: \"${font.family}\";`,\n\t\t\tfont.ascentOverride ? ` ascent-override: ${font.ascentOverride};` : null,\n\t\t\tfont.descentOverride ? ` descent-override: ${font.descentOverride};` : null,\n\t\t\tfont.stretch ? ` font-stretch: ${font.stretch};` : null,\n\t\t\tfont.style ? ` font-style: ${font.style};` : null,\n\t\t\tfont.weight ? ` font-weight: ${font.weight};` : null,\n\t\t\tfont.featureSettings ? ` font-feature-settings: ${font.featureSettings};` : null,\n\t\t\tfont.lineGapOverride ? ` line-gap-override: ${font.lineGapOverride};` : null,\n\t\t\tfont.unicodeRange ? ` unicode-range: ${font.unicodeRange};` : null,\n\t\t\t` src: ${src};`,\n\t\t\t`}`,\n\t\t]).join('\\n')\n\t}\n}\n\n// From https://drafts.csswg.org/css-font-loading/#fontface-interface\nconst defaultFontFaceDescriptors = {\n\tstyle: 'normal',\n\tweight: 'normal',\n\tstretch: 'normal',\n\tunicodeRange: 'U+0-10FFFF',\n\tfeatureSettings: 'normal',\n\tascentOverride: 'normal',\n\tdescentOverride: 'normal',\n\tlineGapOverride: 'normal',\n}\n\n// A FontFace is fully determined by its family and descriptors, so resolved faces can be\n// cached per document and reused across FontManager instances (e.g. editor remounts),\n// turning the per-lookup FontFaceSet scan into an O(1) map lookup. Faces are never removed\n// from `document.fonts` (FontManager.dispose leaves them), so the cache stays valid for the\n// document's lifetime. Keyed per document for cross-window embedding.\nlet fontFaceCacheByDocument = new WeakMap<Document, Map<string, FontFace>>()\n\nfunction getFontFaceCacheKey(font: TLFontFace): string {\n\treturn JSON.stringify([\n\t\tfont.family,\n\t\t...objectMapEntries(defaultFontFaceDescriptors).map(\n\t\t\t([key, defaultValue]) => font[key] ?? defaultValue\n\t\t),\n\t])\n}\n\n/**\n * Resets the per-document font-face cache. Only intended for tests.\n * @internal\n */\nexport function clearFontFaceCacheForTests() {\n\tfontFaceCacheByDocument = new WeakMap()\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAgD;AAChD,mBAAwB;AAExB,mBAMO;AAiBP,MAAM,+BAAoD,EAAE,KAAK,MAAM,OAAU;AACjF,MAAM,oCAA6D,EAAE,KAAK,MAAM,OAAU;AAGnF,MAAM,YAAY;AAAA,EACxB,YACkB,QACA,WAChB;AAFgB;AACA;AAEjB,SAAK,sBAAsB,OAAO,MAAM;AAAA,MACvC;AAAA,MACA,CAAC,UAAmB;AACnB,cAAM,YAAY,KAAK,OAAO,aAAa,KAAK;AAChD,eAAO,UAAU,aAAa,KAAK;AAAA,MACpC;AAAA,MACA;AAAA,QACC,iBAAiB;AAAA,QACjB,iBAAiB,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE;AAAA,MAChE;AAAA,IACD;AAEA,SAAK,0BAA0B,OAAO,MAAM;AAAA,MAC3C,CAAC,OAAkB;AAClB,cAAM,wBAAoB,uBAAS,cAAc,MAAM,KAAK,kBAAkB,EAAE,CAAC;AACjF,mBAAO;AAAA,UACN;AAAA,UACA,MAAM;AACL,kBAAM,SAAS,kBAAkB,IAAI,EAAE,IAAI,CAAC,SAAS,KAAK,aAAa,IAAI,CAAC;AAC5E,mBAAO;AAAA,UACR;AAAA,UACA,EAAE,SAAS,mCAAsB;AAAA,QAClC;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAAA,EA5BkB;AAAA,EACA;AAAA,EA6BlB,UAAU;AACT,SAAK,WAAW,MAAM;AACtB,SAAK,YAAY,MAAM;AACvB,SAAK,sBAAsB;AAC3B,SAAK,0BAA0B;AAAA,EAChC;AAAA,EAEQ;AAAA,EACA;AAAA,EAER,kBAAkB,OAA0C;AAC3D,UAAM,UAAU,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC1D,WAAO,KAAK,oBAAoB,IAAI,OAAO,KAAK;AAAA,EACjD;AAAA,EAEA,mBAAmB,OAA4B;AAC9C,UAAM,UAAU,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC1D,SAAK,wBAAwB,IAAI,OAAO;AAAA,EACzC;AAAA,EAEA,MAAM,gCAAgC,QAAQ,UAAU;AACvD,UAAM,cAAc,oBAAI,IAAgB;AACxC,eAAW,WAAW,KAAK,OAAO,uBAAuB,GAAG;AAC3D,iBAAW,QAAQ,KAAK,kBAAkB,KAAK,OAAO,SAAS,OAAO,CAAE,GAAG;AAC1E,oBAAY,IAAI,IAAI;AAAA,MACrB;AAAA,IACD;AAEA,QAAI,YAAY,OAAO,OAAO;AAC7B;AAAA,IACD;AAEA,UAAM,WAAW,MAAM,KAAK,aAAa,CAAC,SAAS,KAAK,mBAAmB,IAAI,CAAC;AAChF,UAAM,QAAQ,IAAI,QAAQ;AAAA,EAC3B;AAAA,EAEiB,aAAa,IAAI,qBAA+B,aAAa;AAAA,EACtE,aAAa,MAAoC;AACxD,WAAO,KAAK,WAAW,IAAI,IAAI,KAAK;AAAA,EACrC;AAAA,EAEA,mBAAmB,MAAiC;AACnD,UAAM,gBAAgB,KAAK,aAAa,IAAI;AAC5C,QAAI,cAAe,QAAO,cAAc;AAExC,UAAM,WAAW,KAAK,qBAAqB,IAAI;AAC/C,UAAM,QAAmB;AAAA,MACxB,OAAO;AAAA,MACP;AAAA,MACA,gBAAgB,SACd,KAAK,EACL,KAAK,MAAM;AACX,aAAK,OAAO,qBAAqB,EAAE,MAAM,IAAI,QAAQ;AACrD,aAAK,WAAW,OAAO,MAAM,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,QAAQ,EAAE;AAAA,MAC/D,CAAC,EACA,MAAM,CAAC,QAAQ;AACf,gBAAQ,MAAM,GAAG;AACjB,aAAK,WAAW,OAAO,MAAM,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,QAAQ,EAAE;AAAA,MAC/D,CAAC;AAAA,IACH;AAEA,SAAK,WAAW,IAAI,MAAM,KAAK;AAC/B,WAAO,MAAM;AAAA,EACd;AAAA,EAEQ,cAAc,oBAAI,IAAgB;AAAA,EAC1C,aAAa,OAAqB;AACjC,QAAI,CAAC,KAAK,YAAY,MAAM;AAC3B,qBAAe,MAAM;AACpB,YAAI,KAAK,OAAO,WAAY;AAC5B,cAAM,SAAS,KAAK;AACpB,aAAK,cAAc,oBAAI,IAAI;AAC3B,mCAAS,MAAM;AACd,qBAAW,QAAQ,QAAQ;AAC1B,iBAAK,mBAAmB,IAAI;AAAA,UAC7B;AAAA,QACD,CAAC;AAAA,MACF,CAAC;AAAA,IACF;AACA,eAAW,QAAQ,OAAO;AACzB,WAAK,YAAY,IAAI,IAAI;AAAA,IAC1B;AAAA,EACD;AAAA,EAEQ,qBAAqB,MAAkB;AAC9C,UAAM,oBAAoB,KAAK,OAAO,qBAAqB;AAQ3D,QAAI,QAAQ,wBAAwB,IAAI,iBAAiB;AACzD,QAAI,CAAC,OAAO;AACX,cAAQ,oBAAI,IAAI;AAChB,8BAAwB,IAAI,mBAAmB,KAAK;AAAA,IACrD;AACA,UAAM,MAAM,oBAAoB,IAAI;AACpC,UAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,QAAI,OAAQ,QAAO;AAEnB,UAAM,QAAQ,kBAAkB;AAGhC,eAAW,YAAY,OAAO;AAC7B,UACC,SAAS,WAAW,KAAK,cACzB,+BAAiB,0BAA0B,EAAE;AAAA,QAC5C,CAAC,CAACA,MAAK,YAAY,MAAM,SAASA,IAAG,OAAO,KAAKA,IAAG,KAAK;AAAA,MAC1D,GACC;AACD,cAAM,IAAI,KAAK,QAAQ;AACvB,eAAO;AAAA,MACR;AAAA,IACD;AAEA,UAAM,MAAM,KAAK,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI;AACvD,UAAM,WAAW,IAAI,SAAS,KAAK,QAAQ,OAAO,KAAK,UAAU,GAAG,CAAC,KAAK;AAAA,MACzE,OAAG,iCAAmB,4BAA4B,CAACA,SAAQ,KAAKA,IAAG,CAAC;AAAA,MACpE,SAAS;AAAA,IACV,CAAC;AAED,UAAM,IAAI,QAAQ;AAClB,UAAM,IAAI,KAAK,QAAQ;AAEvB,WAAO;AAAA,EACR;AAAA,EAEA,MAAM,yBAAyB,MAAkB;AAChD,UAAM,MAAM,KAAK,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI;AACvD,UAAM,UAAU,MAAM,yBAAY,aAAa,GAAG;AAElD,UAAM,UAAM,sBAAQ;AAAA,MACnB,QAAQ,OAAO;AAAA,MACf,KAAK,IAAI,SAAS,UAAU,KAAK,IAAI,MAAM,MAAM;AAAA,MACjD,KAAK,IAAI,OAAO,QAAQ,KAAK,IAAI,IAAI,MAAM;AAAA,IAC5C,CAAC,EAAE,KAAK,GAAG;AACX,eAAO,sBAAQ;AAAA,MACd;AAAA,MACA,mBAAmB,KAAK,MAAM;AAAA,MAC9B,KAAK,iBAAiB,sBAAsB,KAAK,cAAc,MAAM;AAAA,MACrE,KAAK,kBAAkB,uBAAuB,KAAK,eAAe,MAAM;AAAA,MACxE,KAAK,UAAU,mBAAmB,KAAK,OAAO,MAAM;AAAA,MACpD,KAAK,QAAQ,iBAAiB,KAAK,KAAK,MAAM;AAAA,MAC9C,KAAK,SAAS,kBAAkB,KAAK,MAAM,MAAM;AAAA,MACjD,KAAK,kBAAkB,4BAA4B,KAAK,eAAe,MAAM;AAAA,MAC7E,KAAK,kBAAkB,wBAAwB,KAAK,eAAe,MAAM;AAAA,MACzE,KAAK,eAAe,oBAAoB,KAAK,YAAY,MAAM;AAAA,MAC/D,UAAU,GAAG;AAAA,MACb;AAAA,IACD,CAAC,EAAE,KAAK,IAAI;AAAA,EACb;AACD;AAGA,MAAM,6BAA6B;AAAA,EAClC,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,iBAAiB;AAClB;AAOA,IAAI,0BAA0B,oBAAI,QAAyC;AAE3E,SAAS,oBAAoB,MAA0B;AACtD,SAAO,KAAK,UAAU;AAAA,IACrB,KAAK;AAAA,IACL,OAAG,+BAAiB,0BAA0B,EAAE;AAAA,MAC/C,CAAC,CAAC,KAAK,YAAY,MAAM,KAAK,GAAG,KAAK;AAAA,IACvC;AAAA,EACD,CAAC;AACF;AAMO,SAAS,6BAA6B;AAC5C,4BAA0B,oBAAI,QAAQ;AACvC;",
|
|
6
|
+
"names": ["key"]
|
|
7
7
|
}
|
package/dist-cjs/version.js
CHANGED
|
@@ -22,10 +22,10 @@ __export(version_exports, {
|
|
|
22
22
|
version: () => version
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(version_exports);
|
|
25
|
-
const version = "5.2.0-canary.
|
|
25
|
+
const version = "5.2.0-canary.2f9a2114decd";
|
|
26
26
|
const publishDates = {
|
|
27
27
|
major: "2026-05-06T16:28:18.473Z",
|
|
28
|
-
minor: "2026-06-
|
|
29
|
-
patch: "2026-06-
|
|
28
|
+
minor: "2026-06-19T17:12:33.800Z",
|
|
29
|
+
patch: "2026-06-19T17:12:33.800Z"
|
|
30
30
|
};
|
|
31
31
|
//# sourceMappingURL=version.js.map
|
package/dist-cjs/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.2f9a2114decd'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-06-19T17:12:33.800Z',\n\tpatch: '2026-06-19T17:12:33.800Z',\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
|
@@ -298,7 +298,7 @@ import { LocalIndexedDb, Table } from "./lib/utils/sync/LocalIndexedDb.mjs";
|
|
|
298
298
|
import { uniq } from "./lib/utils/uniq.mjs";
|
|
299
299
|
registerTldrawLibraryVersion(
|
|
300
300
|
"@tldraw/editor",
|
|
301
|
-
"5.2.0-canary.
|
|
301
|
+
"5.2.0-canary.2f9a2114decd",
|
|
302
302
|
"esm"
|
|
303
303
|
);
|
|
304
304
|
import { getColorValue } from "./lib/editor/managers/ThemeManager/defaultThemes.mjs";
|
|
@@ -110,20 +110,31 @@ class FontManager {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
findOrCreateFontFace(font) {
|
|
113
|
-
const
|
|
113
|
+
const containerDocument = this.editor.getContainerDocument();
|
|
114
|
+
let cache = fontFaceCacheByDocument.get(containerDocument);
|
|
115
|
+
if (!cache) {
|
|
116
|
+
cache = /* @__PURE__ */ new Map();
|
|
117
|
+
fontFaceCacheByDocument.set(containerDocument, cache);
|
|
118
|
+
}
|
|
119
|
+
const key = getFontFaceCacheKey(font);
|
|
120
|
+
const cached = cache.get(key);
|
|
121
|
+
if (cached) return cached;
|
|
122
|
+
const fonts = containerDocument.fonts;
|
|
114
123
|
for (const existing of fonts) {
|
|
115
124
|
if (existing.family === font.family && objectMapEntries(defaultFontFaceDescriptors).every(
|
|
116
|
-
([
|
|
125
|
+
([key2, defaultValue]) => existing[key2] === (font[key2] ?? defaultValue)
|
|
117
126
|
)) {
|
|
127
|
+
cache.set(key, existing);
|
|
118
128
|
return existing;
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
const url = this.assetUrls?.[font.src.url] ?? font.src.url;
|
|
122
132
|
const instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {
|
|
123
|
-
...mapObjectMapValues(defaultFontFaceDescriptors, (
|
|
133
|
+
...mapObjectMapValues(defaultFontFaceDescriptors, (key2) => font[key2]),
|
|
124
134
|
display: "swap"
|
|
125
135
|
});
|
|
126
136
|
fonts.add(instance);
|
|
137
|
+
cache.set(key, instance);
|
|
127
138
|
return instance;
|
|
128
139
|
}
|
|
129
140
|
async toEmbeddedCssDeclaration(font) {
|
|
@@ -160,7 +171,20 @@ const defaultFontFaceDescriptors = {
|
|
|
160
171
|
descentOverride: "normal",
|
|
161
172
|
lineGapOverride: "normal"
|
|
162
173
|
};
|
|
174
|
+
let fontFaceCacheByDocument = /* @__PURE__ */ new WeakMap();
|
|
175
|
+
function getFontFaceCacheKey(font) {
|
|
176
|
+
return JSON.stringify([
|
|
177
|
+
font.family,
|
|
178
|
+
...objectMapEntries(defaultFontFaceDescriptors).map(
|
|
179
|
+
([key, defaultValue]) => font[key] ?? defaultValue
|
|
180
|
+
)
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
function clearFontFaceCacheForTests() {
|
|
184
|
+
fontFaceCacheByDocument = /* @__PURE__ */ new WeakMap();
|
|
185
|
+
}
|
|
163
186
|
export {
|
|
164
|
-
FontManager
|
|
187
|
+
FontManager,
|
|
188
|
+
clearFontFaceCacheForTests
|
|
165
189
|
};
|
|
166
190
|
//# sourceMappingURL=FontManager.mjs.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/lib/editor/managers/FontManager/FontManager.ts"],
|
|
4
|
-
"sourcesContent": ["import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'\nimport { AtomMap } from '@tldraw/store'\nimport { TLFontFace, TLShape, TLShapeId } from '@tldraw/tlschema'\nimport {\n\tareArraysShallowEqual,\n\tcompact,\n\tFileHelpers,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n} from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\n\ninterface FontState {\n\treadonly state: 'loading' | 'ready' | 'error'\n\treadonly instance: FontFace\n\treadonly loadingPromise: Promise<void>\n}\n\ninterface ShapeFontFacesCache {\n\tget(id: TLShapeId): TLFontFace[] | undefined\n}\n\ninterface ShapeFontLoadStateCache {\n\tget(id: TLShapeId): (FontState | null)[] | undefined\n}\n\nconst EMPTY_SHAPE_FONT_FACES_CACHE: ShapeFontFacesCache = { get: () => undefined }\nconst EMPTY_SHAPE_FONT_LOAD_STATE_CACHE: ShapeFontLoadStateCache = { get: () => undefined }\n\n/** @public */\nexport class FontManager {\n\tconstructor(\n\t\tprivate readonly editor: Editor,\n\t\tprivate readonly assetUrls?: { [key: string]: string | undefined }\n\t) {\n\t\tthis.shapeFontFacesCache = editor.store.createComputedCache(\n\t\t\t'shape font faces',\n\t\t\t(shape: TLShape) => {\n\t\t\t\tconst shapeUtil = this.editor.getShapeUtil(shape)\n\t\t\t\treturn shapeUtil.getFontFaces(shape)\n\t\t\t},\n\t\t\t{\n\t\t\t\tareResultsEqual: areArraysShallowEqual,\n\t\t\t\tareRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,\n\t\t\t}\n\t\t)\n\n\t\tthis.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(\n\t\t\t(id: TLShapeId) => {\n\t\t\t\tconst fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))\n\t\t\t\treturn computed(\n\t\t\t\t\t'font load state',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst states = fontFacesComputed.get().map((face) => this.getFontState(face))\n\t\t\t\t\t\treturn states\n\t\t\t\t\t},\n\t\t\t\t\t{ isEqual: areArraysShallowEqual }\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n\n\tdispose() {\n\t\tthis.fontStates.clear()\n\t\tthis.fontsToLoad.clear()\n\t\tthis.shapeFontFacesCache = EMPTY_SHAPE_FONT_FACES_CACHE\n\t\tthis.shapeFontLoadStateCache = EMPTY_SHAPE_FONT_LOAD_STATE_CACHE\n\t}\n\n\tprivate shapeFontFacesCache: ShapeFontFacesCache\n\tprivate shapeFontLoadStateCache: ShapeFontLoadStateCache\n\n\tgetShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\treturn this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY\n\t}\n\n\ttrackFontsForShape(shape: TLShape | TLShapeId) {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\tthis.shapeFontLoadStateCache.get(shapeId)\n\t}\n\n\tasync loadRequiredFontsForCurrentPage(limit = Infinity) {\n\t\tconst neededFonts = new Set<TLFontFace>()\n\t\tfor (const shapeId of this.editor.getCurrentPageShapeIds()) {\n\t\t\tfor (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {\n\t\t\t\tneededFonts.add(font)\n\t\t\t}\n\t\t}\n\n\t\tif (neededFonts.size > limit) {\n\t\t\treturn\n\t\t}\n\n\t\tconst promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))\n\t\tawait Promise.all(promises)\n\t}\n\n\tprivate readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')\n\tprivate getFontState(font: TLFontFace): FontState | null {\n\t\treturn this.fontStates.get(font) ?? null\n\t}\n\n\tensureFontIsLoaded(font: TLFontFace): Promise<void> {\n\t\tconst existingState = this.getFontState(font)\n\t\tif (existingState) return existingState.loadingPromise\n\n\t\tconst instance = this.findOrCreateFontFace(font)\n\t\tconst state: FontState = {\n\t\t\tstate: 'loading',\n\t\t\tinstance,\n\t\t\tloadingPromise: instance\n\t\t\t\t.load()\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.editor.getContainerDocument().fonts.add(instance)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(err)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'error' }))\n\t\t\t\t}),\n\t\t}\n\n\t\tthis.fontStates.set(font, state)\n\t\treturn state.loadingPromise\n\t}\n\n\tprivate fontsToLoad = new Set<TLFontFace>()\n\trequestFonts(fonts: TLFontFace[]) {\n\t\tif (!this.fontsToLoad.size) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (this.editor.isDisposed) return\n\t\t\t\tconst toLoad = this.fontsToLoad\n\t\t\t\tthis.fontsToLoad = new Set()\n\t\t\t\ttransact(() => {\n\t\t\t\t\tfor (const font of toLoad) {\n\t\t\t\t\t\tthis.ensureFontIsLoaded(font)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t\tfor (const font of fonts) {\n\t\t\tthis.fontsToLoad.add(font)\n\t\t}\n\t}\n\n\tprivate findOrCreateFontFace(font: TLFontFace) {\n\t\tconst
|
|
5
|
-
"mappings": "AAAA,SAAS,UAAU,aAAa,gBAAgB;AAChD,SAAS,eAAe;AAExB;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AAiBP,MAAM,+BAAoD,EAAE,KAAK,MAAM,OAAU;AACjF,MAAM,oCAA6D,EAAE,KAAK,MAAM,OAAU;AAGnF,MAAM,YAAY;AAAA,EACxB,YACkB,QACA,WAChB;AAFgB;AACA;AAEjB,SAAK,sBAAsB,OAAO,MAAM;AAAA,MACvC;AAAA,MACA,CAAC,UAAmB;AACnB,cAAM,YAAY,KAAK,OAAO,aAAa,KAAK;AAChD,eAAO,UAAU,aAAa,KAAK;AAAA,MACpC;AAAA,MACA;AAAA,QACC,iBAAiB;AAAA,QACjB,iBAAiB,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE;AAAA,MAChE;AAAA,IACD;AAEA,SAAK,0BAA0B,OAAO,MAAM;AAAA,MAC3C,CAAC,OAAkB;AAClB,cAAM,oBAAoB,SAAS,cAAc,MAAM,KAAK,kBAAkB,EAAE,CAAC;AACjF,eAAO;AAAA,UACN;AAAA,UACA,MAAM;AACL,kBAAM,SAAS,kBAAkB,IAAI,EAAE,IAAI,CAAC,SAAS,KAAK,aAAa,IAAI,CAAC;AAC5E,mBAAO;AAAA,UACR;AAAA,UACA,EAAE,SAAS,sBAAsB;AAAA,QAClC;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAAA,EA5BkB;AAAA,EACA;AAAA,EA6BlB,UAAU;AACT,SAAK,WAAW,MAAM;AACtB,SAAK,YAAY,MAAM;AACvB,SAAK,sBAAsB;AAC3B,SAAK,0BAA0B;AAAA,EAChC;AAAA,EAEQ;AAAA,EACA;AAAA,EAER,kBAAkB,OAA0C;AAC3D,UAAM,UAAU,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC1D,WAAO,KAAK,oBAAoB,IAAI,OAAO,KAAK;AAAA,EACjD;AAAA,EAEA,mBAAmB,OAA4B;AAC9C,UAAM,UAAU,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC1D,SAAK,wBAAwB,IAAI,OAAO;AAAA,EACzC;AAAA,EAEA,MAAM,gCAAgC,QAAQ,UAAU;AACvD,UAAM,cAAc,oBAAI,IAAgB;AACxC,eAAW,WAAW,KAAK,OAAO,uBAAuB,GAAG;AAC3D,iBAAW,QAAQ,KAAK,kBAAkB,KAAK,OAAO,SAAS,OAAO,CAAE,GAAG;AAC1E,oBAAY,IAAI,IAAI;AAAA,MACrB;AAAA,IACD;AAEA,QAAI,YAAY,OAAO,OAAO;AAC7B;AAAA,IACD;AAEA,UAAM,WAAW,MAAM,KAAK,aAAa,CAAC,SAAS,KAAK,mBAAmB,IAAI,CAAC;AAChF,UAAM,QAAQ,IAAI,QAAQ;AAAA,EAC3B;AAAA,EAEiB,aAAa,IAAI,QAA+B,aAAa;AAAA,EACtE,aAAa,MAAoC;AACxD,WAAO,KAAK,WAAW,IAAI,IAAI,KAAK;AAAA,EACrC;AAAA,EAEA,mBAAmB,MAAiC;AACnD,UAAM,gBAAgB,KAAK,aAAa,IAAI;AAC5C,QAAI,cAAe,QAAO,cAAc;AAExC,UAAM,WAAW,KAAK,qBAAqB,IAAI;AAC/C,UAAM,QAAmB;AAAA,MACxB,OAAO;AAAA,MACP;AAAA,MACA,gBAAgB,SACd,KAAK,EACL,KAAK,MAAM;AACX,aAAK,OAAO,qBAAqB,EAAE,MAAM,IAAI,QAAQ;AACrD,aAAK,WAAW,OAAO,MAAM,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,QAAQ,EAAE;AAAA,MAC/D,CAAC,EACA,MAAM,CAAC,QAAQ;AACf,gBAAQ,MAAM,GAAG;AACjB,aAAK,WAAW,OAAO,MAAM,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,QAAQ,EAAE;AAAA,MAC/D,CAAC;AAAA,IACH;AAEA,SAAK,WAAW,IAAI,MAAM,KAAK;AAC/B,WAAO,MAAM;AAAA,EACd;AAAA,EAEQ,cAAc,oBAAI,IAAgB;AAAA,EAC1C,aAAa,OAAqB;AACjC,QAAI,CAAC,KAAK,YAAY,MAAM;AAC3B,qBAAe,MAAM;AACpB,YAAI,KAAK,OAAO,WAAY;AAC5B,cAAM,SAAS,KAAK;AACpB,aAAK,cAAc,oBAAI,IAAI;AAC3B,iBAAS,MAAM;AACd,qBAAW,QAAQ,QAAQ;AAC1B,iBAAK,mBAAmB,IAAI;AAAA,UAC7B;AAAA,QACD,CAAC;AAAA,MACF,CAAC;AAAA,IACF;AACA,eAAW,QAAQ,OAAO;AACzB,WAAK,YAAY,IAAI,IAAI;AAAA,IAC1B;AAAA,EACD;AAAA,EAEQ,qBAAqB,MAAkB;AAC9C,UAAM,
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'\nimport { AtomMap } from '@tldraw/store'\nimport { TLFontFace, TLShape, TLShapeId } from '@tldraw/tlschema'\nimport {\n\tareArraysShallowEqual,\n\tcompact,\n\tFileHelpers,\n\tmapObjectMapValues,\n\tobjectMapEntries,\n} from '@tldraw/utils'\nimport type { Editor } from '../../Editor'\n\ninterface FontState {\n\treadonly state: 'loading' | 'ready' | 'error'\n\treadonly instance: FontFace\n\treadonly loadingPromise: Promise<void>\n}\n\ninterface ShapeFontFacesCache {\n\tget(id: TLShapeId): TLFontFace[] | undefined\n}\n\ninterface ShapeFontLoadStateCache {\n\tget(id: TLShapeId): (FontState | null)[] | undefined\n}\n\nconst EMPTY_SHAPE_FONT_FACES_CACHE: ShapeFontFacesCache = { get: () => undefined }\nconst EMPTY_SHAPE_FONT_LOAD_STATE_CACHE: ShapeFontLoadStateCache = { get: () => undefined }\n\n/** @public */\nexport class FontManager {\n\tconstructor(\n\t\tprivate readonly editor: Editor,\n\t\tprivate readonly assetUrls?: { [key: string]: string | undefined }\n\t) {\n\t\tthis.shapeFontFacesCache = editor.store.createComputedCache(\n\t\t\t'shape font faces',\n\t\t\t(shape: TLShape) => {\n\t\t\t\tconst shapeUtil = this.editor.getShapeUtil(shape)\n\t\t\t\treturn shapeUtil.getFontFaces(shape)\n\t\t\t},\n\t\t\t{\n\t\t\t\tareResultsEqual: areArraysShallowEqual,\n\t\t\t\tareRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,\n\t\t\t}\n\t\t)\n\n\t\tthis.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(\n\t\t\t(id: TLShapeId) => {\n\t\t\t\tconst fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))\n\t\t\t\treturn computed(\n\t\t\t\t\t'font load state',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst states = fontFacesComputed.get().map((face) => this.getFontState(face))\n\t\t\t\t\t\treturn states\n\t\t\t\t\t},\n\t\t\t\t\t{ isEqual: areArraysShallowEqual }\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n\n\tdispose() {\n\t\tthis.fontStates.clear()\n\t\tthis.fontsToLoad.clear()\n\t\tthis.shapeFontFacesCache = EMPTY_SHAPE_FONT_FACES_CACHE\n\t\tthis.shapeFontLoadStateCache = EMPTY_SHAPE_FONT_LOAD_STATE_CACHE\n\t}\n\n\tprivate shapeFontFacesCache: ShapeFontFacesCache\n\tprivate shapeFontLoadStateCache: ShapeFontLoadStateCache\n\n\tgetShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\treturn this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY\n\t}\n\n\ttrackFontsForShape(shape: TLShape | TLShapeId) {\n\t\tconst shapeId = typeof shape === 'string' ? shape : shape.id\n\t\tthis.shapeFontLoadStateCache.get(shapeId)\n\t}\n\n\tasync loadRequiredFontsForCurrentPage(limit = Infinity) {\n\t\tconst neededFonts = new Set<TLFontFace>()\n\t\tfor (const shapeId of this.editor.getCurrentPageShapeIds()) {\n\t\t\tfor (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {\n\t\t\t\tneededFonts.add(font)\n\t\t\t}\n\t\t}\n\n\t\tif (neededFonts.size > limit) {\n\t\t\treturn\n\t\t}\n\n\t\tconst promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))\n\t\tawait Promise.all(promises)\n\t}\n\n\tprivate readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')\n\tprivate getFontState(font: TLFontFace): FontState | null {\n\t\treturn this.fontStates.get(font) ?? null\n\t}\n\n\tensureFontIsLoaded(font: TLFontFace): Promise<void> {\n\t\tconst existingState = this.getFontState(font)\n\t\tif (existingState) return existingState.loadingPromise\n\n\t\tconst instance = this.findOrCreateFontFace(font)\n\t\tconst state: FontState = {\n\t\t\tstate: 'loading',\n\t\t\tinstance,\n\t\t\tloadingPromise: instance\n\t\t\t\t.load()\n\t\t\t\t.then(() => {\n\t\t\t\t\tthis.editor.getContainerDocument().fonts.add(instance)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(err)\n\t\t\t\t\tthis.fontStates.update(font, (s) => ({ ...s, state: 'error' }))\n\t\t\t\t}),\n\t\t}\n\n\t\tthis.fontStates.set(font, state)\n\t\treturn state.loadingPromise\n\t}\n\n\tprivate fontsToLoad = new Set<TLFontFace>()\n\trequestFonts(fonts: TLFontFace[]) {\n\t\tif (!this.fontsToLoad.size) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tif (this.editor.isDisposed) return\n\t\t\t\tconst toLoad = this.fontsToLoad\n\t\t\t\tthis.fontsToLoad = new Set()\n\t\t\t\ttransact(() => {\n\t\t\t\t\tfor (const font of toLoad) {\n\t\t\t\t\t\tthis.ensureFontIsLoaded(font)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t\tfor (const font of fonts) {\n\t\t\tthis.fontsToLoad.add(font)\n\t\t}\n\t}\n\n\tprivate findOrCreateFontFace(font: TLFontFace) {\n\t\tconst containerDocument = this.editor.getContainerDocument()\n\n\t\t// `findOrCreateFontFace` runs for every font on every editor mount, and a fresh\n\t\t// editor (e.g. switching documents) gets a fresh FontManager with no memory of the\n\t\t// previous one. The dedup below is an O(n) scan of the document's whole FontFaceSet,\n\t\t// so without a cache that scan re-ran on every mount (measurably expensive on mobile\n\t\t// Safari). Cache the resolved FontFace per document so repeated lookups - and\n\t\t// remounts - are O(1). Keyed per document for cross-window embedding.\n\t\tlet cache = fontFaceCacheByDocument.get(containerDocument)\n\t\tif (!cache) {\n\t\t\tcache = new Map()\n\t\t\tfontFaceCacheByDocument.set(containerDocument, cache)\n\t\t}\n\t\tconst key = getFontFaceCacheKey(font)\n\t\tconst cached = cache.get(key)\n\t\tif (cached) return cached\n\n\t\tconst fonts = containerDocument.fonts\n\t\t// On a cache miss we still scan once, so font faces added outside this manager\n\t\t// (e.g. preloaded fonts) are reused rather than duplicated.\n\t\tfor (const existing of fonts) {\n\t\t\tif (\n\t\t\t\texisting.family === font.family &&\n\t\t\t\tobjectMapEntries(defaultFontFaceDescriptors).every(\n\t\t\t\t\t([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tcache.set(key, existing)\n\t\t\t\treturn existing\n\t\t\t}\n\t\t}\n\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {\n\t\t\t...mapObjectMapValues(defaultFontFaceDescriptors, (key) => font[key]),\n\t\t\tdisplay: 'swap',\n\t\t})\n\n\t\tfonts.add(instance)\n\t\tcache.set(key, instance)\n\n\t\treturn instance\n\t}\n\n\tasync toEmbeddedCssDeclaration(font: TLFontFace) {\n\t\tconst url = this.assetUrls?.[font.src.url] ?? font.src.url\n\t\tconst dataUrl = await FileHelpers.urlToDataUrl(url)\n\n\t\tconst src = compact([\n\t\t\t`url(\"${dataUrl}\")`,\n\t\t\tfont.src.format ? `format(${font.src.format})` : null,\n\t\t\tfont.src.tech ? `tech(${font.src.tech})` : null,\n\t\t]).join(' ')\n\t\treturn compact([\n\t\t\t`@font-face {`,\n\t\t\t` font-family: \"${font.family}\";`,\n\t\t\tfont.ascentOverride ? ` ascent-override: ${font.ascentOverride};` : null,\n\t\t\tfont.descentOverride ? ` descent-override: ${font.descentOverride};` : null,\n\t\t\tfont.stretch ? ` font-stretch: ${font.stretch};` : null,\n\t\t\tfont.style ? ` font-style: ${font.style};` : null,\n\t\t\tfont.weight ? ` font-weight: ${font.weight};` : null,\n\t\t\tfont.featureSettings ? ` font-feature-settings: ${font.featureSettings};` : null,\n\t\t\tfont.lineGapOverride ? ` line-gap-override: ${font.lineGapOverride};` : null,\n\t\t\tfont.unicodeRange ? ` unicode-range: ${font.unicodeRange};` : null,\n\t\t\t` src: ${src};`,\n\t\t\t`}`,\n\t\t]).join('\\n')\n\t}\n}\n\n// From https://drafts.csswg.org/css-font-loading/#fontface-interface\nconst defaultFontFaceDescriptors = {\n\tstyle: 'normal',\n\tweight: 'normal',\n\tstretch: 'normal',\n\tunicodeRange: 'U+0-10FFFF',\n\tfeatureSettings: 'normal',\n\tascentOverride: 'normal',\n\tdescentOverride: 'normal',\n\tlineGapOverride: 'normal',\n}\n\n// A FontFace is fully determined by its family and descriptors, so resolved faces can be\n// cached per document and reused across FontManager instances (e.g. editor remounts),\n// turning the per-lookup FontFaceSet scan into an O(1) map lookup. Faces are never removed\n// from `document.fonts` (FontManager.dispose leaves them), so the cache stays valid for the\n// document's lifetime. Keyed per document for cross-window embedding.\nlet fontFaceCacheByDocument = new WeakMap<Document, Map<string, FontFace>>()\n\nfunction getFontFaceCacheKey(font: TLFontFace): string {\n\treturn JSON.stringify([\n\t\tfont.family,\n\t\t...objectMapEntries(defaultFontFaceDescriptors).map(\n\t\t\t([key, defaultValue]) => font[key] ?? defaultValue\n\t\t),\n\t])\n}\n\n/**\n * Resets the per-document font-face cache. Only intended for tests.\n * @internal\n */\nexport function clearFontFaceCacheForTests() {\n\tfontFaceCacheByDocument = new WeakMap()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["key"]
|
|
7
7
|
}
|
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "5.2.0-canary.
|
|
1
|
+
const version = "5.2.0-canary.2f9a2114decd";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2026-05-06T16:28:18.473Z",
|
|
4
|
-
minor: "2026-06-
|
|
5
|
-
patch: "2026-06-
|
|
4
|
+
minor: "2026-06-19T17:12:33.800Z",
|
|
5
|
+
patch: "2026-06-19T17:12:33.800Z"
|
|
6
6
|
};
|
|
7
7
|
export {
|
|
8
8
|
publishDates,
|
package/dist-esm/version.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.2f9a2114decd'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-06-19T17:12:33.800Z',\n\tpatch: '2026-06-19T17:12:33.800Z',\n}\n"],
|
|
5
5
|
"mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (editor).",
|
|
4
|
-
"version": "5.2.0-canary.
|
|
4
|
+
"version": "5.2.0-canary.2f9a2114decd",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"@tiptap/core": "^3.12.1",
|
|
50
50
|
"@tiptap/pm": "^3.12.1",
|
|
51
51
|
"@tiptap/react": "^3.12.1",
|
|
52
|
-
"@tldraw/state": "5.2.0-canary.
|
|
53
|
-
"@tldraw/state-react": "5.2.0-canary.
|
|
54
|
-
"@tldraw/store": "5.2.0-canary.
|
|
55
|
-
"@tldraw/tlschema": "5.2.0-canary.
|
|
56
|
-
"@tldraw/utils": "5.2.0-canary.
|
|
57
|
-
"@tldraw/validate": "5.2.0-canary.
|
|
52
|
+
"@tldraw/state": "5.2.0-canary.2f9a2114decd",
|
|
53
|
+
"@tldraw/state-react": "5.2.0-canary.2f9a2114decd",
|
|
54
|
+
"@tldraw/store": "5.2.0-canary.2f9a2114decd",
|
|
55
|
+
"@tldraw/tlschema": "5.2.0-canary.2f9a2114decd",
|
|
56
|
+
"@tldraw/utils": "5.2.0-canary.2f9a2114decd",
|
|
57
|
+
"@tldraw/validate": "5.2.0-canary.2f9a2114decd",
|
|
58
58
|
"classnames": "^2.5.1",
|
|
59
59
|
"eventemitter3": "^4.0.7",
|
|
60
60
|
"idb": "^7.1.1",
|
|
@@ -82,6 +82,9 @@
|
|
|
82
82
|
"resize-observer-polyfill": "^1.5.1",
|
|
83
83
|
"vitest": "^4.1.7"
|
|
84
84
|
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=22.12.0"
|
|
87
|
+
},
|
|
85
88
|
"module": "dist-esm/index.mjs",
|
|
86
89
|
"source": "src/index.ts",
|
|
87
90
|
"exports": {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { IndexKey } from '@tldraw/utils'
|
|
10
10
|
import { Mock, Mocked, vi } from 'vitest'
|
|
11
11
|
import { Editor } from '../../Editor'
|
|
12
|
-
import { FontManager } from './FontManager'
|
|
12
|
+
import { clearFontFaceCacheForTests, FontManager } from './FontManager'
|
|
13
13
|
|
|
14
14
|
// Mock the Editor class
|
|
15
15
|
vi.mock('../../Editor')
|
|
@@ -73,6 +73,7 @@ describe('FontManager', () => {
|
|
|
73
73
|
|
|
74
74
|
beforeEach(() => {
|
|
75
75
|
vi.clearAllMocks()
|
|
76
|
+
clearFontFaceCacheForTests()
|
|
76
77
|
|
|
77
78
|
mockAssetUrls = {
|
|
78
79
|
'test-font.woff2': 'https://example.com/fonts/test-font.woff2',
|
|
@@ -314,4 +315,26 @@ describe('FontManager', () => {
|
|
|
314
315
|
await expect(fontManager.ensureFontIsLoaded(minimalFont)).resolves.toBeUndefined()
|
|
315
316
|
})
|
|
316
317
|
})
|
|
318
|
+
|
|
319
|
+
describe('findOrCreateFontFace caching', () => {
|
|
320
|
+
it('reuses a font face across manager instances instead of recreating it on remount', async () => {
|
|
321
|
+
const font = createMockFont()
|
|
322
|
+
await fontManager.ensureFontIsLoaded(font)
|
|
323
|
+
expect(global.FontFace).toHaveBeenCalledTimes(1)
|
|
324
|
+
|
|
325
|
+
// A fresh manager on the same document simulates an editor remount.
|
|
326
|
+
const remounted = new FontManager(editor, mockAssetUrls)
|
|
327
|
+
await remounted.ensureFontIsLoaded(font)
|
|
328
|
+
|
|
329
|
+
// The per-document cache lets the second manager reuse the existing face
|
|
330
|
+
// rather than re-scanning document.fonts and creating a new one.
|
|
331
|
+
expect(global.FontFace).toHaveBeenCalledTimes(1)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('creates separate font faces for fonts with different descriptors', async () => {
|
|
335
|
+
await fontManager.ensureFontIsLoaded(createMockFont({ weight: 'normal' }))
|
|
336
|
+
await fontManager.ensureFontIsLoaded(createMockFont({ weight: 'bold' }))
|
|
337
|
+
expect(global.FontFace).toHaveBeenCalledTimes(2)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
317
340
|
})
|
|
@@ -145,7 +145,26 @@ export class FontManager {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
private findOrCreateFontFace(font: TLFontFace) {
|
|
148
|
-
const
|
|
148
|
+
const containerDocument = this.editor.getContainerDocument()
|
|
149
|
+
|
|
150
|
+
// `findOrCreateFontFace` runs for every font on every editor mount, and a fresh
|
|
151
|
+
// editor (e.g. switching documents) gets a fresh FontManager with no memory of the
|
|
152
|
+
// previous one. The dedup below is an O(n) scan of the document's whole FontFaceSet,
|
|
153
|
+
// so without a cache that scan re-ran on every mount (measurably expensive on mobile
|
|
154
|
+
// Safari). Cache the resolved FontFace per document so repeated lookups - and
|
|
155
|
+
// remounts - are O(1). Keyed per document for cross-window embedding.
|
|
156
|
+
let cache = fontFaceCacheByDocument.get(containerDocument)
|
|
157
|
+
if (!cache) {
|
|
158
|
+
cache = new Map()
|
|
159
|
+
fontFaceCacheByDocument.set(containerDocument, cache)
|
|
160
|
+
}
|
|
161
|
+
const key = getFontFaceCacheKey(font)
|
|
162
|
+
const cached = cache.get(key)
|
|
163
|
+
if (cached) return cached
|
|
164
|
+
|
|
165
|
+
const fonts = containerDocument.fonts
|
|
166
|
+
// On a cache miss we still scan once, so font faces added outside this manager
|
|
167
|
+
// (e.g. preloaded fonts) are reused rather than duplicated.
|
|
149
168
|
for (const existing of fonts) {
|
|
150
169
|
if (
|
|
151
170
|
existing.family === font.family &&
|
|
@@ -153,6 +172,7 @@ export class FontManager {
|
|
|
153
172
|
([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)
|
|
154
173
|
)
|
|
155
174
|
) {
|
|
175
|
+
cache.set(key, existing)
|
|
156
176
|
return existing
|
|
157
177
|
}
|
|
158
178
|
}
|
|
@@ -164,6 +184,7 @@ export class FontManager {
|
|
|
164
184
|
})
|
|
165
185
|
|
|
166
186
|
fonts.add(instance)
|
|
187
|
+
cache.set(key, instance)
|
|
167
188
|
|
|
168
189
|
return instance
|
|
169
190
|
}
|
|
@@ -205,3 +226,27 @@ const defaultFontFaceDescriptors = {
|
|
|
205
226
|
descentOverride: 'normal',
|
|
206
227
|
lineGapOverride: 'normal',
|
|
207
228
|
}
|
|
229
|
+
|
|
230
|
+
// A FontFace is fully determined by its family and descriptors, so resolved faces can be
|
|
231
|
+
// cached per document and reused across FontManager instances (e.g. editor remounts),
|
|
232
|
+
// turning the per-lookup FontFaceSet scan into an O(1) map lookup. Faces are never removed
|
|
233
|
+
// from `document.fonts` (FontManager.dispose leaves them), so the cache stays valid for the
|
|
234
|
+
// document's lifetime. Keyed per document for cross-window embedding.
|
|
235
|
+
let fontFaceCacheByDocument = new WeakMap<Document, Map<string, FontFace>>()
|
|
236
|
+
|
|
237
|
+
function getFontFaceCacheKey(font: TLFontFace): string {
|
|
238
|
+
return JSON.stringify([
|
|
239
|
+
font.family,
|
|
240
|
+
...objectMapEntries(defaultFontFaceDescriptors).map(
|
|
241
|
+
([key, defaultValue]) => font[key] ?? defaultValue
|
|
242
|
+
),
|
|
243
|
+
])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resets the per-document font-face cache. Only intended for tests.
|
|
248
|
+
* @internal
|
|
249
|
+
*/
|
|
250
|
+
export function clearFontFaceCacheForTests() {
|
|
251
|
+
fontFaceCacheByDocument = new WeakMap()
|
|
252
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '5.2.0-canary.
|
|
4
|
+
export const version = '5.2.0-canary.2f9a2114decd'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2026-05-06T16:28:18.473Z',
|
|
7
|
-
minor: '2026-06-
|
|
8
|
-
patch: '2026-06-
|
|
7
|
+
minor: '2026-06-19T17:12:33.800Z',
|
|
8
|
+
patch: '2026-06-19T17:12:33.800Z',
|
|
9
9
|
}
|