simvx.graphics.draw2d_text

MSDF text rendering for Draw2D – the ONE 2D text builder.

This is the single glyph layout + measure + emit path for the whole engine. It feeds both the legacy flag-OFF Draw2D op stream (draw_text appends a TEXT :class:Op) and the flag-ON item pipeline (which re-runs the same layout natively via :func:layout_glyph_run). The desktop overlay TextRenderer.draw_text path it replaced is deleted; only the MSDF atlas generation in :mod:text_renderer survives (both paths share it).

Three text-layout reconciliations:

  • Font-size unit = font_scale * 16 is canonical (_LOGICAL_BASE = 16); the old divergent 14 is replaced so a label renders the same pixel height whichever node draws it and whichever backend. The * sy content-scale factor multiplies on top (applied by the caller / the _xf_sc factor).

  • Kerning is ported from the deleted overlay path – the cursor advances by font.get_kerning(prev, ch) * scale between glyph pairs, so the kerned advance is pixel-identical to the old overlay (0.00 px diff).

  • Quad rounding = ceil (fully contains the glyph + padding) replaces the old max(1.0, ...); align spelling = 'centre' (UK, repo rule) – the banned US 'center' is accepted transitionally and mapped to 'centre'.

Text never silently goes blank. A character the primary font lacks is drawn from the font fallback chain; one no available font can draw at all is drawn as a hollow box in the text’s own colour, and reported once per codepoint, rather than advancing the cursor over an invisible gap. Text drawn large enough to read them writes the codepoint out, one box per byte, so the character can be identified from the screen rather than guessed at.

Module Contents

Classes

Draw2DTextMixin

Mixin providing MSDF text rendering and measurement for Draw2D.

Functions

layout_glyph_run

Lay out text into MSDF glyph quads in LOCAL space (the item path).

Data

log

API

simvx.graphics.draw2d_text.log

‘getLogger(…)’

class simvx.graphics.draw2d_text.Draw2DTextMixin

Mixin providing MSDF text rendering and measurement for Draw2D.

classmethod set_font(path: str | None = None, size: int = 48) None

Load an MSDF font atlas via the shared TextRenderer.

classmethod draw_text(text, pos=None, *, colour=None, scale=1.0, rect=None, alignment='left', vertical_alignment='top', fit_to_width=False, min_scale=None, outline=0.0, outline_colour=None, screen_space=False, cell_width=0.0)

Draw text at pos or inside rect with optional alignment.

screen_space=True bypasses the active Camera2D transform (the screen-pinned HUD-label case – the behaviour Text2D’s deleted overlay pass had: text stays fixed while the world camera pans).

Two positioning modes:

  • pos=(x, y): text anchored at the given coordinate. alignment ('left'/'centre'/'right') anchors each line’s left edge / centre / right edge on x (the overlay-parity point anchor).

  • rect=(x, y, w, h): text positioned by alignment and vertical_alignment (top/centre/bottom) inside the rect. fit_to_width=True shrinks scale so the text fits rect.w, clamped to min_scale.

outline (in glyph-em units, e.g. 0.08) draws a 4-direction offset copy of the run in outline_colour (default opaque black) UNDER the main run – the common port “readable text on any background” pattern. min_scale=None applies the ~10px readable safety net; an explicit value honours the caller’s floor exactly.

cell_width is the pitch of the character grid this text sits on, in the pixels it is laid out in (before any Draw2D transform), and is how a caller that has a grid says so: a terminal, a code view, a tabular readout. It changes one thing: a character no font can draw occupies exactly one cell, drawn as the largest form that fits it, instead of taking the room a box asks for in proportional text. The columns after it stay on the grid, and nothing it draws reaches the column beside it, which is what a caller that places every character itself needs. Text that is not on a grid leaves it at zero and is drawn exactly as before.

Both the US 'center' and the UK 'centre' are accepted; 'centre' is canonical (repo spelling).

classmethod text_height(text, scale=1.0)

Height of text in pixels at scale.

Multi-line strings (containing \n) accumulate line heights using the font’s line-height metric. Returns 0 for empty input.

classmethod text_size(text, scale=1.0)

Return (width, height) in pixels at scale.

classmethod fit_scale(text, max_width, *, base_scale=1.0, min_scale=None, cell_width=0.0)

Largest scale ≤ base_scale that fits text within max_width.

Returns base_scale when the text already fits or when inputs are degenerate. The returned value matches what :meth:draw_text would actually use, so callers can compute width metrics that align with what’s drawn.

min_scale semantics mirror :meth:draw_text:

  • None (default): clamps from below by the readable-pixel floor (~10px) so the returned scale never produces illegible MSDF output.

  • explicit value: the caller’s floor is honoured exactly, bypassing the readable safety net.

classmethod text_width(text, scale=1, *, cell_width=0.0)

Width of text in pixels at scale.

Multi-line strings (containing \n) return the widest line; newlines themselves contribute no horizontal advance. The walk is

Meth:

_layout_run’s, without the geometry: the same advances, and the same rule for which neighbours a kerning pair reaches across.

cell_width is the caller’s character-grid pitch, in the pixels this returns, and means what it does on :meth:draw_text: a boxed character is reserved one cell, so the width comes back as the run is drawn.

A character the primary face lacks but another draws is measured at the borrowing face’s advance without consulting the fallback chain here: the walk below is preceded by _ensure_with_fallback, which resolves the character through that chain and packs its glyph into the atlas, so it is an ordinary atlas cell by the time the walk reaches it.

Func:

~simvx.core.text.measure_text_width asks the same lookup directly and gets the same face at the same size, which is why the two agree. The chain refuses to lend a glyph for a character defined to occupy no space, so neither walk can be handed one to measure.

simvx.graphics.draw2d_text.layout_glyph_run(text, pos=None, *, colour=None, scale=1.0, rect=None, alignment='left', vertical_alignment='top', fit_to_width=False, min_scale=None, outline=0.0, outline_colour=None, cell_width=0.0)

Lay out text into MSDF glyph quads in LOCAL space (the item path).

The native-emission counterpart of :meth:Draw2DTextMixin.draw_text: it runs the SAME single layout (kerning, *16 unit, ceil, centre) but without baking any Draw2D transform and without appending an Op – it returns (verts, indices) so the item builder can stow them as a GLYPH item’s geometry (geometry stays camera-/parent-free, the node’s transform rides the item’s transform row). Returns ([], []) when there is no font or no visible glyphs.

Because both paths funnel through :meth:Draw2DTextMixin._layout_run, the item-path glyph geometry is byte-identical to the op-path geometry at the same position/scale – which is what makes flag-ON text match flag-OFF. That covers the missing-glyph box’s choice of form as well: no form_scale is passed here because there is no transform to exclude, and the op path passes its own untransformed scale so it reaches the same choice. cell_width rides through unchanged, so a monospace caller reaches the same grid whichever path draws it.