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.
144 lines
3.8 KiB
144 lines
3.8 KiB
|
3 months ago
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|