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 immediate Draw2D op stream (text appends a
TEXT :class:Op) and the 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 * 16is canonical (_LOGICAL_BASE = 16); the old divergent14is replaced so a label renders the same pixel height whichever node draws it and whichever backend. The* sycontent-scale factor multiplies on top (applied by the caller / the_xf_scfactor).Kerning is ported from the deleted overlay path – the cursor advances by
font.get_kerning(prev, ch) * scalebetween 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 oldmax(1.0, ...). Alignment is an :class:~simvx.core.HAlign/- class:
~simvx.core.VAlignmember or its plain string.
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¶
Mixin providing MSDF text rendering and measurement for Draw2D. |
Functions¶
Lay out |
Data¶
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 text(text, pos=None, *, fill=None, scale=1.0, align: simvx.core.alignment.HAlign | str = HAlign.LEFT, valign: simvx.core.alignment.VAlign | str = VAlign.TOP, stroke=None, stroke_width=0.0, rect=None, fit_to_width=False, min_scale=None, cell_width=0.0, rotation=0.0)¶
Draw text at
posor insiderect, in the colourfill.Two positioning modes:
pos=(x, y): text anchored at the given coordinate.align(:class:~simvx.core.HAlign) anchors each line’s left edge / centre / right edge onx.rect=(x, y, w, h): text positioned byalignandvalign(:class:~simvx.core.VAlign) inside the rect.fit_to_width=Trueshrinksscaleso the text fitsrect’s width, clamped tomin_scale.
scaleis a multiplier on the logical 16-pixel em, soscale=2draws at roughly 32 pixels.stroke_width(in glyph-em units, e.g.0.08) draws a 4-direction offset copy of the run instroke(default opaque black at the fill’s alpha) UNDER the main run: readable text on any background.min_scale=Noneapplies the ~10px readable safety net; an explicit value honours the caller’s floor exactly.cell_widthis 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.rotation(radians) turns the run about its anchor,posor therect’s top-left corner, in the same sense as a sprite’s rotation.alignandvaligntake the member or its plain string.
- classmethod text_height(text, scale=1.0)¶
Height of
textin pixels atscale.Multi-line strings (containing
\n) accumulate line heights using the font’s line-height metric. Returns 0 for empty input.
- classmethod text_ascent(scale=1.0)¶
Distance in pixels from the top of a line to its baseline at
scale.
- classmethod text_size(text, scale=1.0)¶
Return
(width, height)in pixels atscale.
- classmethod text_pixels(text: str, scale: float = 1.0, colour: Any = (1.0, 1.0, 1.0, 1.0)) numpy.ndarray¶
textrasterised into an RGBAuint8image, top row first, sized to fit it.For text that belongs to an image: a card face, a sign, a label on a sprite that rotates. Baked in, it is part of the image and costs one textured quad wherever the image is drawn. It is drawn from the font 2D text uses, on every backend.
scaleis the multiplier- Meth:
texttakes, soscale=1is 16 px, andcolouris 0-1 RGB(A) written as given, the way a texture’s own pixels are.
One line. The image keeps a 2 px margin so ink that overhangs the pen or the ascender is not clipped, so its pen origin, the left end of the baseline, is pixel
(2, 2 + text_ascent(scale)).Raises: ValueError: text holds a line break. RuntimeError: no font is loaded, so there is nothing to draw with.
- classmethod fit_scale(text, max_width, *, base_scale=1.0, min_scale=None, cell_width=0.0)¶
Largest scale ≤
base_scalethat fitstextwithinmax_width.Returns
base_scalewhen the text already fits or when inputs are degenerate. The returned value matches what :meth:textwould actually use, so callers can compute width metrics that align with what’s drawn.min_scalesemantics mirror :meth: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
textin pixels atscale.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_widthis the caller’s character-grid pitch, in the pixels this returns, and means what it does on :meth: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_widthasks 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: simvx.core.alignment.HAlign | str = HAlign.LEFT, vertical_alignment: simvx.core.alignment.VAlign | str = VAlign.TOP, fit_to_width=False, min_scale=None, outline=0.0, outline_colour=None, cell_width=0.0, rotation=0.0, pixel_origin=(0.0, 0.0), pixel_scale=1.0)¶
Lay out
textinto MSDF glyph quads in LOCAL space (the item path).The native-emission counterpart of :meth:
Draw2DTextMixin.text: it runs the SAME single layout (kerning,*16unit,ceil,centre) but without baking anyDraw2Dtransform and without appending anOp– it returns(verts, indices)so the item builder can stow them as aGLYPHitem’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: noform_scaleis 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_widthrides through unchanged, so a monospace caller reaches the same grid whichever path draws it.pixel_originandpixel_scaleare the translation and the uniform scale the caller will bake the returned quads by. The run is laid out atscale * pixel_scale, snapped to whole pixels at that size, and returned divided bypixel_scale, so the bake puts every glyph on the pixel grid at the size it is shown. That is the op path’s own behaviour, which lays out after folding its transform in; laying out at 1x and multiplying would put most glyphs between pixels at 1.25x and 1.5x.