@manifold-studio/typeface
Version:
Font loading and text-to-3D conversion for Manifold Studio
630 lines (629 loc) • 17.2 kB
JavaScript
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