Skip to main content

Text Measurement

Accurate text width matters: the selection box has to hug the rendered glyphs, and wrapping depends on real advances. The editor measures text through a pluggable shaper so the selection bounds match whichever backend actually draws the text.

The WASM shaperโ€‹

@oh-just-another/text-wasm provides a WasmTextShaper implementing the TextShaper interface from @oh-just-another/renderer-core. Until a WASM module is loaded it returns a synchronous geometric estimate (so first paint isn't blank); loadModule(...) (URL, bytes, or Response) swaps in a real engine at runtime, and results are cached in a small LRU keyed on the (text, font) pair:

import { WasmTextShaper } from "@oh-just-another/text-wasm";

const shaper = new WasmTextShaper();
await shaper.loadModule("/text-shaper.wasm"); // optional โ€” fallback estimate until loaded

// Or load the module shipped with the package:
const bundled = await WasmTextShaper.loadBundled();

The drop-in <Editor> (from @oh-just-another/editor) wires this up for you: it detects WebGL2/WASM capability and loads the bundled MSDF-capable shaper automatically, so labels render in their final font from the first frame.

Geometric fallback in the scene kernelโ€‹

@oh-just-another/scene exposes a TextMeasurer hook. When a host installs one (backed by the renderer's measureText), the text bounder uses it for accurate width; when absent (headless / tests) it falls back to a chars ร— fontSize ร— factor estimate:

import { setTextMeasurer, type TextMeasurer } from "@oh-just-another/scene";

const measurer: TextMeasurer = (text, fontFamily, fontSize, opts) => {
// return measured width in world px, or null to defer to the estimate
return null;
};
setTextMeasurer(measurer); // pass null to clear

The opts argument carries bold / italic so the measured width matches the rendered width โ€” weight and style change glyph advances.