import { layoutWithLines, prepareWithSegments, } from '@chenglou/pretext'; /** * High-performance text layout engine using pretext */ export interface LayoutLine { text: string; width: number; chars: Array<{ char: string; x: number; width: number; }>; } export interface LayoutResult { lines: LayoutLine[]; totalHeight: number; } export class TextLayoutEngine { #segmenter: Intl.Segmenter; constructor(locale?: string) { // Use Intl.Segmenter for grapheme-level segmentation // Pretext uses this internally, so we align with it. this.#segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' }); } /** * Measure and layout text within a given width */ layout(text: string, font: string, width: number, lineHeight: number): LayoutResult { if (!text) { return { lines: [], totalHeight: 0 }; } // Use prepareWithSegments to get segment information const prepared = prepareWithSegments(text, font); const { lines, height } = layoutWithLines(prepared, width, lineHeight); // Access internal pretext data for character-level offsets const internal = prepared as any; const breakableFitAdvances = internal.breakableFitAdvances as (number[] | null)[]; const widths = internal.widths as number[]; const resultLines: LayoutLine[] = lines.map(line => { const chars: LayoutLine['chars'] = []; let currentX = 0; const start = line.start; const end = line.end; // Iterate through segments in the line for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) { const segmentText = prepared.segments[sIdx]; if (segmentText === undefined) continue; // Get graphemes for this segment const segments = this.#segmenter.segment(segmentText); const graphemes = Array.from(segments, s => s.segment); // Get widths/advances for graphemes in this segment const advances = breakableFitAdvances[sIdx]; const gStart = sIdx === start.segmentIndex ? start.graphemeIndex : 0; const gEnd = sIdx === end.segmentIndex ? end.graphemeIndex : graphemes.length; for (let gIdx = gStart; gIdx < gEnd; gIdx++) { const char = graphemes[gIdx]; // advances is null for single-grapheme or non-breakable segments. // In both cases the whole segment width belongs to the single grapheme. const charWidth = advances != null ? advances[gIdx]! : widths[sIdx]!; chars.push({ char, x: currentX, width: charWidth, }); currentX += charWidth; } } return { text: line.text, width: line.width, chars, }; }); return { lines: resultLines, totalHeight: height, }; } }