/** * TextLayout * * Text layout engine for MSDF text rendering. * Handles line breaking, alignment, and glyph positioning. * * MSDF 文本渲染的文本布局引擎 * 处理换行、对齐和字形定位 */ import type { MSDFFont, IMSDFGlyph } from './MSDFFont'; import { EAlignType, EVertAlignType } from '../core/FieldTypes'; /** * Positioned glyph for rendering * 用于渲染的定位字形 */ export interface IPositionedGlyph { /** Glyph data | 字形数据 */ glyph: IMSDFGlyph; /** X position in pixels | X 位置(像素) */ x: number; /** Y position in pixels | Y 位置(像素) */ y: number; /** Glyph width in pixels | 字形宽度(像素) */ width: number; /** Glyph height in pixels | 字形高度(像素) */ height: number; /** UV coordinates [u0, v0, u1, v1] | UV 坐标 */ uv: [number, number, number, number]; } /** * Layout line * 布局行 */ interface ILayoutLine { /** Glyphs in this line | 此行中的字形 */ glyphs: IPositionedGlyph[]; /** Line width in pixels | 行宽(像素) */ width: number; /** Line start Y position | 行起始 Y 位置 */ y: number; } /** * Text layout options * 文本布局选项 */ export interface ITextLayoutOptions { /** Font to use | 使用的字体 */ font: MSDFFont; /** Text content | 文本内容 */ text: string; /** Font size in pixels | 字体大小(像素) */ fontSize: number; /** Maximum width (for word wrap) | 最大宽度(用于换行) */ maxWidth?: number; /** Maximum height | 最大高度 */ maxHeight?: number; /** Horizontal alignment | 水平对齐 */ align?: EAlignType; /** Vertical alignment | 垂直对齐 */ valign?: EVertAlignType; /** Line height multiplier | 行高倍数 */ lineHeight?: number; /** Letter spacing in pixels | 字间距(像素) */ letterSpacing?: number; /** Word wrap enabled | 是否启用换行 */ wordWrap?: boolean; /** Single line mode | 单行模式 */ singleLine?: boolean; } /** * Text layout result * 文本布局结果 */ export interface ITextLayoutResult { /** Positioned glyphs ready for rendering | 准备渲染的定位字形 */ glyphs: IPositionedGlyph[]; /** Total width of laid out text | 布局文本的总宽度 */ width: number; /** Total height of laid out text | 布局文本的总高度 */ height: number; /** Number of lines | 行数 */ lineCount: number; } /** * Layout text into positioned glyphs * 将文本布局为定位字形 */ export function layoutText(options: ITextLayoutOptions): ITextLayoutResult { const { font, text, fontSize, maxWidth = Infinity, maxHeight = Infinity, align = EAlignType.Left, valign = EVertAlignType.Top, lineHeight = 1.2, letterSpacing = 0, wordWrap = false, singleLine = false } = options; if (!text || !font) { return { glyphs: [], width: 0, height: 0, lineCount: 0 }; } const metrics = font.metrics; const atlas = font.atlas; // Calculate scale from em units to pixels const scale = fontSize / metrics.emSize; const lineHeightPx = fontSize * lineHeight; // Atlas dimensions for UV calculation const atlasWidth = atlas.width; const atlasHeight = atlas.height; const yFlip = atlas.yOrigin === 'bottom'; const lines: ILayoutLine[] = []; let currentLine: IPositionedGlyph[] = []; let currentX = 0; let currentY = 0; let maxLineWidth = 0; let prevCharCode = 0; // Process each character for (let i = 0; i < text.length; i++) { const char = text[i]; const charCode = char.charCodeAt(0); // Handle newline if (char === '\n') { if (singleLine) continue; lines.push({ glyphs: currentLine, width: currentX, y: currentY }); maxLineWidth = Math.max(maxLineWidth, currentX); currentLine = []; currentX = 0; currentY += lineHeightPx; prevCharCode = 0; continue; } // Handle carriage return if (char === '\r') continue; // Get glyph const glyph = font.getGlyph(charCode); if (!glyph) { // Try space as fallback const spaceGlyph = font.getGlyph(32); if (spaceGlyph) { currentX += spaceGlyph.advance * scale + letterSpacing; } prevCharCode = charCode; continue; } // Apply kerning if (prevCharCode) { currentX += font.getKerning(prevCharCode, charCode) * scale; } // Check word wrap const glyphAdvance = glyph.advance * scale + letterSpacing; if (wordWrap && !singleLine && currentX + glyphAdvance > maxWidth && currentLine.length > 0) { // Word wrap - start new line lines.push({ glyphs: currentLine, width: currentX, y: currentY }); maxLineWidth = Math.max(maxLineWidth, currentX); currentLine = []; currentX = 0; currentY += lineHeightPx; // Check max height if (currentY + lineHeightPx > maxHeight) { break; } } // Position glyph if it has atlas bounds if (glyph.planeBounds && glyph.atlasBounds) { const pb = glyph.planeBounds; const ab = glyph.atlasBounds; // Calculate glyph position and size const glyphX = currentX + pb.left * scale; const glyphY = currentY + (metrics.ascender - pb.top) * scale; const glyphWidth = (pb.right - pb.left) * scale; const glyphHeight = (pb.top - pb.bottom) * scale; // Calculate UV coordinates let u0 = ab.left / atlasWidth; let v0 = ab.bottom / atlasHeight; let u1 = ab.right / atlasWidth; let v1 = ab.top / atlasHeight; // Flip V if Y origin is top if (!yFlip) { v0 = 1 - v0; v1 = 1 - v1; [v0, v1] = [v1, v0]; } currentLine.push({ glyph, x: glyphX, y: glyphY, width: glyphWidth, height: glyphHeight, uv: [u0, v0, u1, v1] }); } currentX += glyphAdvance; prevCharCode = charCode; } // Add last line if (currentLine.length > 0 || lines.length === 0) { lines.push({ glyphs: currentLine, width: currentX, y: currentY }); maxLineWidth = Math.max(maxLineWidth, currentX); } const totalHeight = currentY + lineHeightPx; const lineCount = lines.length; // Apply horizontal alignment for (const line of lines) { let offsetX = 0; if (align === EAlignType.Center) { offsetX = (maxWidth === Infinity ? 0 : (maxWidth - line.width) / 2); } else if (align === EAlignType.Right) { offsetX = maxWidth === Infinity ? 0 : (maxWidth - line.width); } for (const glyph of line.glyphs) { glyph.x += offsetX; } } // Apply vertical alignment let offsetY = 0; if (valign === EVertAlignType.Middle) { offsetY = (maxHeight === Infinity ? 0 : (maxHeight - totalHeight) / 2); } else if (valign === EVertAlignType.Bottom) { offsetY = maxHeight === Infinity ? 0 : (maxHeight - totalHeight); } if (offsetY !== 0) { for (const line of lines) { for (const glyph of line.glyphs) { glyph.y += offsetY; } } } // Flatten glyphs const allGlyphs: IPositionedGlyph[] = []; for (const line of lines) { allGlyphs.push(...line.glyphs); } return { glyphs: allGlyphs, width: maxLineWidth, height: totalHeight, lineCount }; } /** * Measure text dimensions without full layout * 测量文本尺寸(不进行完整布局) */ export function measureText(font: MSDFFont, text: string, fontSize: number, letterSpacing: number = 0): { width: number; height: number } { if (!text || !font) { return { width: 0, height: 0 }; } const metrics = font.metrics; const scale = fontSize / metrics.emSize; let width = 0; let prevCharCode = 0; for (let i = 0; i < text.length; i++) { const charCode = text.charCodeAt(i); const glyph = font.getGlyph(charCode); if (glyph) { if (prevCharCode) { width += font.getKerning(prevCharCode, charCode) * scale; } width += glyph.advance * scale + letterSpacing; } prevCharCode = charCode; } return { width, height: fontSize * 1.2 }; }