UNPKG

@manifold-studio/typeface

Version:

Font loading and text-to-3D conversion for Manifold Studio

630 lines (629 loc) 17.2 kB
var T = Object.defineProperty; var R = (i, t, r) => t in i ? T(i, t, { enumerable: !0, configurable: !0, writable: !0, value: r }) : i[t] = r; var y = (i, t, r) => R(i, typeof t != "symbol" ? t + "" : t, r); import { CrossSection as A } from "@manifold-studio/wrapper"; import E from "opentype.js"; function I(i, t = {}) { const { holeThreshold: r = 0.9, sampleCount: o = 100, debug: e = !1 } = t; return i.length === 0 ? [] : i.length === 1 ? [{ index: 0, polygon: i[0], area: Math.abs(S(i[0])), isHole: !1, confidence: 1, debugInfo: e ? { method: "single-polygon" } : void 0 }] : B(i, r, o, e); } function B(i, t, r, o) { const e = i.map((n, a) => ({ index: a, polygon: n, area: Math.abs(S(n)), isHole: !1, confidence: 0, debugInfo: o ? {} : void 0 })), s = []; for (let n = 0; n < e.length; n++) { let a = !1; for (let l = 0; l < e.length; l++) { if (n === l) continue; if (v( e[n].polygon, e[l].polygon, Math.min(r, 50) // Use fewer samples for containment check ) >= 0.8 && e[l].area > e[n].area) { a = !0; break; } } a || s.push(n); } for (const n of s) e[n].isHole = !1, e[n].confidence = 1, o && (e[n].debugInfo.method = "outer-contour", e[n].debugInfo.sampleCount = r); for (let n = 0; n < e.length; n++) { if (s.includes(n)) continue; let a = -1, l = 0; for (const c of s) { const d = v( e[n].polygon, e[c].polygon, r ); d > l && (l = d, a = c); } a >= 0 && l >= t ? (e[n].isHole = !0, e[n].confidence = Math.min(l / t, 1), o && (e[n].debugInfo.overlapRatio = l, e[n].debugInfo.method = "contained-hole", e[n].debugInfo.sampleCount = r, e[n].debugInfo.containerIndex = a)) : (e[n].isHole = !1, e[n].confidence = a >= 0 ? Math.min((1 - l) / (1 - t), 1) : 1, o && (e[n].debugInfo.overlapRatio = l, e[n].debugInfo.method = "separate-solid", e[n].debugInfo.sampleCount = r)); } return e; } function v(i, t, r) { if (r <= 0) return 0; let o = 0, e = 0; for (let s = 0; s < r * 2 && e < r; s++) { const n = k(i); n && (e++, L(n, t) && o++); } return e > 0 ? o / e : 0; } function k(i) { if (i.length < 3) return null; const t = C(i), r = 50; for (let o = 0; o < r; o++) { const e = { x: t.minX + Math.random() * (t.maxX - t.minX), y: t.minY + Math.random() * (t.maxY - t.minY) }; if (L(e, i)) return e; } return null; } function L(i, t) { if (t.length < 3) return !1; let r = !1; const { x: o, y: e } = i; for (let s = 0, n = t.length - 1; s < t.length; n = s++) { const a = t[s].x, l = t[s].y, c = t[n].x, d = t[n].y; l > e != d > e && o < (c - a) * (e - l) / (d - l) + a && (r = !r); } return r; } function C(i) { if (i.length === 0) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; let t = i[0].x, r = i[0].y, o = i[0].x, e = i[0].y; for (let s = 1; s < i.length; s++) { const { x: n, y: a } = i[s]; n < t && (t = n), n > o && (o = n), a < r && (r = a), a > e && (e = a); } return { minX: t, minY: r, maxX: o, maxY: e }; } function S(i) { if (i.length < 3) return 0; let t = 0; for (let r = 0; r < i.length; r++) { const o = (r + 1) % i.length; t += i[r].x * i[o].y, t -= i[o].x * i[r].y; } return t / 2; } class b extends Error { constructor(t, r, o, e) { super(t), this.fontName = r, this.attemptedUrls = o, this.lastError = e, this.name = "FontLoadError"; } } class O extends Error { constructor(t, r) { super(`Font loading timeout after ${r}ms for '${t}'`), this.name = "FontTimeoutError"; } } const p = class p { constructor() { y(this, "fontCache", /* @__PURE__ */ new Map()); y(this, "loadingPromises", /* @__PURE__ */ new Map()); } /** * Get list of available font names * @returns Array of font names that can be loaded */ getAvailableFonts() { return p.AVAILABLE_FONTS.map((t) => t.name); } /** * Load a font by name with caching and fallback support * * @param fontName - Name of the font to load (must be in registry) * @returns Promise that resolves to the loaded font * @throws {FontLoadError} When font loading fails from all URLs * @throws {FontTimeoutError} When font loading exceeds timeout * @throws {Error} When font is not found in registry */ async loadFont(t) { const r = this.fontCache.get(t); if (r) return r; const o = this.loadingPromises.get(t); if (o) return o; const e = p.AVAILABLE_FONTS.find((n) => n.name === t); if (!e) throw new Error(`Font '${t}' not found. Available fonts: ${p.AVAILABLE_FONTS.map((n) => n.name).join(", ")}`); const s = this.loadFontFromUrl(e); this.loadingPromises.set(t, s); try { const n = await s; return this.fontCache.set(t, n), this.loadingPromises.delete(t), n; } catch (n) { throw this.loadingPromises.delete(t), n; } } /** * Load a font from a URL with fallback support */ async loadFontFromUrl(t) { const r = [t.url, ...t.fallbackUrls || []], o = []; if (r.length === 0) throw new b( `No URLs available for font '${t.name}'`, t.name, [] ); for (let e = 0; e < r.length; e++) { const s = r[e], n = e === r.length - 1; try { const a = { ...t, url: s }; return await this.loadSingleUrl(a); } catch (a) { const l = a instanceof Error ? a : new Error(String(a)); if (o.push(l), console.warn(`Failed to load font from ${s}: ${l.message}`), n) throw new b( `Failed to load font '${t.name}' from all ${r.length} URLs`, t.name, r, l ); } } throw new b( `Unexpected error loading font '${t.name}'`, t.name, r, o[o.length - 1] ); } /** * Load a font from a single URL with timeout handling */ async loadSingleUrl(t, r = 3e4) { return new Promise((o, e) => { let s = !1; const n = setTimeout(() => { s || (s = !0, e(new O(t.name, r))); }, r), a = (c) => { s || (s = !0, clearTimeout(n), o(c)); }, l = (c) => { s || (s = !0, clearTimeout(n), e(c)); }; try { this.isBrowser() ? this.loadFontInBrowser(t, a, l) : this.loadFontInNode(t, a, l).catch(l); } catch (c) { const d = c instanceof Error ? c : new Error(String(c)); l(new Error(`Failed to load font '${t.name}': ${d.message}`)); } }); } /** * Load font in browser environment using fetch */ async loadFontInBrowser(t, r, o) { try { const e = await fetch(t.url, { mode: "cors", headers: { Accept: "application/font-ttf, application/octet-stream, */*" } }); if (!e.ok) throw new Error(`HTTP ${e.status}: ${e.statusText}`); const s = await e.arrayBuffer(), n = E.parse(s); if (!n || !n.names) throw new Error("Invalid font file - OpenType.js could not parse the font"); r({ info: t, font: n, loadedAt: Date.now() }); } catch (e) { const s = e instanceof Error ? e : new Error(String(e)); o(new Error(`Browser font loading failed for '${t.name}': ${s.message}`)); } } /** * Load font in Node.js environment */ async loadFontInNode(t, r, o) { var e; try { if (typeof process < "u" && ((e = process.versions) != null && e.node)) try { const s = await fetch(t.url); if (!s.ok) { o(new Error(`Failed to download font '${t.name}': ${s.status} ${s.statusText}`)); return; } const n = await s.arrayBuffer(), a = E.parse(n); if (!a) { o(new Error(`Font parsing failed for '${t.name}': No font object returned`)); return; } r({ info: t, font: a, loadedAt: Date.now() }); } catch (s) { const n = s instanceof Error ? s.message : String(s); o(new Error(`Node.js font loading failed for '${t.name}': ${n}`)); } else E.load(t.url, (s, n) => { if (s) { const a = s.message || String(s); o(new Error(`Font loading fallback failed for '${t.name}': ${a}`)); return; } if (!n) { o(new Error(`Font parsing failed for '${t.name}': No font object returned`)); return; } r({ info: t, font: n, loadedAt: Date.now() }); }); } catch (s) { const n = s instanceof Error ? s : new Error(String(s)); o(new Error(`Node.js font loading setup failed for '${t.name}': ${n.message}`)); } } /** * Detect if we're running in a browser environment */ isBrowser() { return typeof window < "u" && typeof fetch < "u"; } /** * Clear font cache */ clearCache() { this.fontCache.clear(), console.log("Font cache cleared"); } /** * Get cache status */ getCacheStatus() { return Array.from(this.fontCache.entries()).map(([t, r]) => ({ fontName: t, loadedAt: new Date(r.loadedAt), family: r.info.family })); } }; // Fonts available - using proven working URLs from prototype testing y(p, "AVAILABLE_FONTS", [ { name: "Inter", // Match the name in font-registry.ts family: "Inter", weight: "Variable", url: "https://cdn.jsdelivr.net/npm/inter-font@3.19.0/Inter-VariableFont_slnt,wght.ttf" } ]); let w = p; const $ = [ { name: "Inter", family: "Inter", weight: "Variable", url: "https://cdn.jsdelivr.net/npm/inter-font@3.19.0/Inter-VariableFont_slnt,wght.ttf" } ]; class U { constructor() { y(this, "fontResolver"); y(this, "customFonts", /* @__PURE__ */ new Map()); y(this, "loadedFonts", /* @__PURE__ */ new Map()); y(this, "initializationPromise", null); this.fontResolver = new w(), w.AVAILABLE_FONTS.length = 0, w.AVAILABLE_FONTS.push(...$); } /** * Register a custom font */ registerFont(t, r, o = {}) { const e = { name: t, url: r, family: o.family || t, weight: o.weight, style: o.style, fallbackUrls: o.fallbackUrls }; this.customFonts.set(t, e); const s = w.AVAILABLE_FONTS.findIndex((n) => n.name === t); s >= 0 ? w.AVAILABLE_FONTS[s] = e : w.AVAILABLE_FONTS.push(e); } /** * Initialize fonts by loading all default and registered custom fonts */ async initialize() { return this.initializationPromise ? this.initializationPromise : (this.initializationPromise = this.loadAllFonts(), this.initializationPromise); } /** * Ensure fonts are ready (same as initialize - idempotent) */ async ensureReady() { return this.initialize(); } /** * Check if fonts have been initialized */ isReady() { return this.initializationPromise !== null && this.loadedFonts.size > 0; } /** * Get list of available font names */ list() { const t = $.map((o) => o.name), r = Array.from(this.customFonts.keys()); return [...t, ...r]; } /** * Get a loaded font by name */ getFont(t) { return this.loadedFonts.get(t) || null; } /** * Check if a specific font is loaded */ isFontLoaded(t) { return this.loadedFonts.has(t); } /** * Load all fonts (default + custom) */ async loadAllFonts() { const r = this.list().map(async (o) => { try { const e = await this.fontResolver.loadFont(o); this.loadedFonts.set(o, e); } catch { } }); if (await Promise.allSettled(r), this.loadedFonts.size === 0) throw new Error("Failed to load any fonts. Check your network connection."); } /** * Clear all loaded fonts and reset initialization */ reset() { this.loadedFonts.clear(), this.initializationPromise = null, this.fontResolver.clearCache(); } /** * Get registry status for debugging */ getStatus() { return { isReady: this.isReady(), loadedFonts: Array.from(this.loadedFonts.keys()), availableFonts: this.list(), customFonts: Array.from(this.customFonts.keys()) }; } } const f = new U(); function z(i, t, r = {}) { const { fontSize: o = 12, letterSpacing: e = 1, align: s = "left", subdivisionSteps: n = 10 } = r, a = f.getFont(t); if (!a) throw new Error( `Font '${t}' not loaded. Available fonts: ${f.list().join(", ")}` ); const l = M(i, a, o, e, n); if (l.length === 0) return new A([]); const c = I(l); if (c.length === 0) return new A([]); const d = []; for (let h = 0; h < c.length; h++) { const u = c[h]; if (!u.polygon || u.polygon.length < 3 || u.polygon.some((x) => !x || x.x === void 0 || x.y === void 0)) continue; let F = u.polygon.map((x) => [x.x, x.y]); const P = N(F) > 0; u.isHole ? P && F.reverse() : P || F.reverse(), d.push(F); } if (d.length === 0) return new A([]); let m = new A(d); if (s !== "left") try { const h = m.bounds(), u = h.max[0] - h.min[0]; if (!(isNaN(u) || !isFinite(u))) { let g = 0; s === "center" ? g = -u / 2 : s === "right" && (g = -u), g !== 0 && isFinite(g) && (m = m.translate([g, 0])); } } catch { } return m; } function N(i) { if (i.length < 3) return 0; let t = 0; for (let r = 0; r < i.length; r++) { const o = (r + 1) % i.length; t += i[r][0] * i[o][1], t -= i[o][0] * i[r][1]; } return t / 2; } function M(i, t, r, o, e) { const s = t.font, n = r / s.unitsPerEm; let a = 0; const l = []; for (let c = 0; c < i.length; c++) { const d = i[c]; if (d === " ") { const F = s.charToGlyph(" ").advanceWidth || s.unitsPerEm * 0.25; a += F * n * o; continue; } const m = s.charToGlyph(d); if (!m) { console.warn(`Glyph not found for character: ${d}`); continue; } const h = m.getPath(a, 0, r), u = V(h, e); l.push(...u); const g = m.advanceWidth || 0; a += g * n * o; } return l; } function V(i, t) { const r = []; let o = []; for (const e of i.commands) switch (e.type) { case "M": o.length > 0 && (r.push([...o]), o = []), o.push({ x: e.x, y: -e.y }); break; case "L": o.push({ x: e.x, y: -e.y }); break; case "Q": const s = j( o[o.length - 1], { x: e.x1, y: -e.y1 }, // Flip Y axis { x: e.x, y: -e.y }, // Flip Y axis t ); o.push(...s.slice(1)); break; case "C": const n = X( o[o.length - 1], { x: e.x1, y: -e.y1 }, // Flip Y axis { x: e.x2, y: -e.y2 }, // Flip Y axis { x: e.x, y: -e.y }, // Flip Y axis t ); o.push(...n.slice(1)); break; case "Z": o.length > 2 && r.push([...o]), o = []; break; } return o.length > 2 && r.push(o), r; } function j(i, t, r, o) { const e = []; for (let s = 0; s <= o; s++) { const n = s / o, a = (1 - n) * (1 - n) * i.x + 2 * (1 - n) * n * t.x + n * n * r.x, l = (1 - n) * (1 - n) * i.y + 2 * (1 - n) * n * t.y + n * n * r.y; e.push({ x: a, y: l }); } return e; } function X(i, t, r, o, e) { const s = []; for (let n = 0; n <= e; n++) { const a = n / e, l = (1 - a) ** 3 * i.x + 3 * (1 - a) ** 2 * a * t.x + 3 * (1 - a) * a ** 2 * r.x + a ** 3 * o.x, c = (1 - a) ** 3 * i.y + 3 * (1 - a) ** 2 * a * t.y + 3 * (1 - a) * a ** 2 * r.y + a ** 3 * o.y; s.push({ x: l, y: c }); } return s; } function D(i) { return (t, r) => { if (!f.isReady()) throw new Error( "Fonts not initialized. Call 'await fonts.ensureReady()' before using fontLoader." ); if (!f.isFontLoaded(i)) { const o = f.list(); throw new Error( `Font '${i}' not available. Available fonts: ${o.join(", ")}` ); } return z(t, i, r); }; } function G(i, t, r = {}) { f.registerFont(i, t, r); } const Q = { /** * Initialize fonts by loading all default and registered custom fonts */ async initialize() { return f.initialize(); }, /** * Ensure fonts are ready (same as initialize - idempotent) */ async ensureReady() { return f.ensureReady(); }, /** * Check if fonts have been initialized */ isReady() { return f.isReady(); }, /** * Get list of available font names */ list() { return f.list(); }, /** * Check if a specific font is loaded */ isFontLoaded(i) { return f.isFontLoaded(i); }, /** * Get registry status for debugging */ getStatus() { return f.getStatus(); }, /** * Reset font registry (for testing) */ reset() { f.reset(); } }; export { $ as DEFAULT_FONTS, b as FontLoadError, O as FontTimeoutError, D as fontLoader, Q as fonts, G as registerFont }; //# sourceMappingURL=index.js.map