import { Entity, type ContentProjection } from '../tree/Entity'; import { MSDFFont } from '@vectojs/text'; export interface MSDFTextEntityOptions { font: MSDFFont; texture: TexImageSource; fallbackFont?: string; fontSize?: number; color?: string; lineHeight?: number; letterSpacing?: number; /** Wrap boundary in logical pixels. Defaults to 1000. */ maxWidth?: number; /** Layout height limit in logical pixels. Defaults to 1000. */ maxHeight?: number; /** * Horizontal alignment. `'justify'` stretches every wrapped line flush to * {@link maxWidth} (the paragraph-final and newline-ended lines stay ragged); * `'left'` (default) leaves them ragged. */ textAlign?: 'left' | 'justify'; } export declare class MSDFTextEntity extends Entity { private font; private texture; private fallbackFont; private fontSize; color: string; private letterSpacing; private lineHeight?; private maxWidth; private maxHeight; private textAlign; private hyphenator; private layoutText; private text; private lastRenderedSeqId; /** Bumped by {@link queueLayout}; read by `Scene` to skip an unchanged sync. */ private contentEpoch; private atlasDecodeTarget; private atlasDecodeHandler; private rgbColorCache; private fontStringCache; private layoutResult; /** * Visual rows rebuilt from {@link layoutResult} (see * {@link rebuildProjectionLines}). Empty until a layout reply lands and the * reply's shaped glyphs can be mapped back to the source text 1:1. */ private projectionLines; constructor(text: string, options: MSDFTextEntityOptions); /** * Repaint once the atlas raster decodes. * * The WebGL backend refuses to upload a not-yet-decoded atlas (it would pin an * empty texture in its identity cache forever), so the upload has to happen on * a LATER frame — and nothing else schedules one. Layout marks the scene dirty * when the worker replies, which for a network-served atlas is long before the * image lands, so the scene is already idle by then. * * Measured on Chromium and Firefox (2026-07-31) with a 600 ms atlas: the * scene's own rAF loop never uploaded a decoded atlas in EITHER render mode. * `onDemand` skips idle frames outright; `always` throttles to the idle FPS * floor when idle, so whether it recovers is down to whether a throttled tick happens to * land after the decode — Chromium got one, Firefox did not. Neither is a * mechanism, which is why this listener exists rather than relying on the * frame loop to come back around. * * Only `HTMLImageElement`-shaped sources have a decode to wait for; a canvas, * `ImageBitmap`, or `VideoFrame` atlas is ready on arrival. */ private watchAtlasDecode; private detachAtlasDecodeListener; /** Change the wrap boundary and re-run layout for the current text. */ setMaxWidth(maxWidth: number): void; /** * Set horizontal alignment (`'justify'` stretches wrapped lines flush to * {@link setMaxWidth}'s width; the last line stays ragged) and re-run layout. */ setTextAlign(align: 'left' | 'justify'): void; /** * Plug a hyphenator (word → parts). Break opportunities are inserted as soft * hyphens (U+00AD) into the string sent to layout, so a word that doesn't fit * can break with a visible hyphen. Soft hyphens already present in the text * work without one. Pass `null` to disable. The original text is preserved * for accessibility — only the layout string carries the hyphens. */ setHyphenator(fn: ((word: string) => string[]) | null): void; setText(text: string): void; /** * Recompute {@link layoutText} from {@link text}: with a hyphenator active, * split each whitespace-delimited word and rejoin its parts with U+00AD so * the worker sees the break opportunities. Without one, the layout string is * the text unchanged. */ private rebuildLayoutText; private queueLayout; /** * Mirror the rendered text into the DOM content layer: find-in-page, screen * readers, crawlers, and translation see the same string the canvas draws. * * `baseline` + `lineHeight` are always emitted (they come from the font * metrics, no layout reply needed), so the DOM line boxes at least land on * the canvas rhythm: the first baseline at `ascender × fontSize` and every * row advancing `(ascender − descender) × fontSize`. Once a layout reply is * in AND its shaped glyphs map back to the source 1:1 (unshaped LTR text — * bidi, shaping, soft hyphens or `\r` all fall back to the coarse branch), * per-line carriers pin each row's baseline exactly to the painted glyphs. */ getContentProjection(): ContentProjection | null; /** * Group the worker's positioned glyphs into the same visual rows the canvas * draws. Only runs when the reply's glyph sequence equals the source string * (one glyph per source char, no bidi reordering, no shaping, no soft * hyphens, no `\r`) — only then do glyph offsets line up with source offsets * byte-for-byte, which is what keeps find-in-page and the Scene's dev-mode * equality check correct. Every other text falls back to the coarse branch's * `baseline` + `lineHeight`, which still pins the row rhythm. */ private rebuildProjectionLines; getContentEpoch(): number; isPointInside(globalX: number, globalY: number): boolean; render(renderer: any): void; destroy(): void; }