forked from mengyxu/noob-components
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
3.8 KiB
143 lines
3.8 KiB
/** |
|
* 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<string, PreparedTextWithSegments>(); |
|
|
|
/** |
|
* 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; |
|
}
|
|
|