/** * Text measurement utilities using @chenglou/pretext * Provides "shrink wrap" width measurement via walkLineRanges * * IMPORTANT: All functions cache PreparedText handles internally. * prepareWithSegments() calls are expensive (Canvas measureText), so we cache * by text+font key to maximize reuse. */ import { prepareWithSegments, layout, walkLineRanges, type PreparedTextWithSegments, } from "@chenglou/pretext"; /** * Cache for PreparedText handles. * Key: `${text}|${font}`, Value: PreparedTextWithSegments (the superset type) */ const preparedCache = new Map(); /** * Get or create a cached PreparedText handle. * Uses prepareWithSegments since it returns the superset type that works * with both layout() and walkLineRanges(). */ function getPrepared(text: string, font: string): PreparedTextWithSegments { const cacheKey = `${text}|${font}`; let prepared = preparedCache.get(cacheKey); if (!prepared) { prepared = prepareWithSegments(text, font); preparedCache.set(cacheKey, prepared); // Limit cache size to prevent memory leaks (simple eviction) if (preparedCache.size > 10000) { // Simple strategy: clear oldest half when limit reached const keys = preparedCache.keys(); let removed = 0; const targetRemoval = preparedCache.size / 2; for (const key of keys) { if (removed >= targetRemoval) break; preparedCache.delete(key); removed++; } } } return prepared; } /** * Clear the entire prepared text cache. * Call this if font configuration changes globally. */ export function clearPreparedCache() { preparedCache.clear(); } /** * Get cache statistics (for debugging) */ export function getPreparedCacheStats() { return { size: preparedCache.size, }; } export interface TextMeasurement { /** Minimum width to contain all lines (widest line wins) */ shrinkWrapWidth: number; /** Height at a given maxWidth */ heightAtWidth: (maxWidth: number) => number; /** Line count at a given maxWidth */ lineCountAtWidth: (maxWidth: number) => number; } /** * Prepare text for measurement. Results are cached internally. * @param text - The text to measure (may contain \n) * @param font - CSS font string */ export function measureText(text: string, font: string): TextMeasurement { if (!text) { return { shrinkWrapWidth: 0, heightAtWidth: () => 0, lineCountAtWidth: () => 0, }; } const prepared = getPrepared(text, font); // Find shrink-wrap width: maximum line width across all lines let shrinkWrapWidth = 0; walkLineRanges(prepared, 10000, (line) => { if (line.width > shrinkWrapWidth) { shrinkWrapWidth = line.width; } }); return { shrinkWrapWidth, heightAtWidth: (maxWidth: number) => { const result = layout(prepared, maxWidth, 20); // 20 = default lineHeight return result.height; }, lineCountAtWidth: (maxWidth: number) => { const result = layout(prepared, maxWidth, 20); return result.lineCount; }, }; } /** * Measure text and get shrink-wrap width in one call. * Uses cached PreparedText handle for performance. */ export function measureShrinkWrapWidth(text: string, font: string): number { if (!text) return 0; const prepared = getPrepared(text, font); let maxW = 0; walkLineRanges(prepared, 10000, (line) => { if (line.width > maxW) maxW = line.width; }); return maxW; } /** * Measure text height at a specific column width. * Uses cached PreparedText handle for performance. */ export function measureTextHeight( text: string, font: string, maxWidth: number, lineHeight: number = 20 ): number { if (!text) return 0; const prepared = getPrepared(text, font); const result = layout(prepared, maxWidth, lineHeight); return result.height; }