test: fix TextLayoutEngine tests — correct jsdom directive placement and canvas mock setup
fix: correct grapheme-width fallback in TextLayoutEngine for null breakableFitAdvances
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user