simvx.core.text.msdf¶
True MSDF atlas generation from vector contours.
Generates multi-channel signed distance fields from FreeType glyph outlines. Each RGB channel encodes distance to a different set of edges, enabling sharp corner reconstruction in the fragment shader via median filtering.
Module Contents¶
Classes¶
Atlas region for a packed glyph. |
|
MSDF font atlas with incremental shelf-based bin packing. |
|
Font atlas using FreeType hinted bitmap rendering (no SDF). |
Functions¶
Whether Unicode defines ch to leave no mark of its own. |
|
Whether ch is meant to draw nothing, rather than be missing a glyph. |
|
Whether ch occupies no horizontal space at all. |
|
Outline of the box drawn in place of a character no font can supply. |
|
The hex digits drawn inside the box for ch, as they are laid out. |
|
Boxes drawn in place of ch: one per byte of its codepoint, so two or three. |
|
Height the digits in a hex box reach, as a fraction of the em. |
|
Whether text at em_pixels is large enough to write a codepoint out. |
|
The room ch is reserved in each form, as a fraction of the em. |
|
Room reserved for ch when nothing can draw it, as a fraction of the em. |
|
Collapse an MSDF atlas to the single distance field the shader reads. |
|
Every atlas cell that makes up what is drawn in place of ch. |
|
Rasterize text from an MSDF atlas into an RGBA uint8 image (CPU, backend-agnostic). |
Data¶
API¶
- simvx.core.text.msdf.log¶
‘getLogger(…)’
- simvx.core.text.msdf.MISSING_GLYPH_KEY¶
‘\ufdd0’
- simvx.core.text.msdf.MISSING_GLYPH_HEX_KEY¶
‘\ufdd1’
- simvx.core.text.msdf.MISSING_GLYPH_ADVANCE¶
0.6
- simvx.core.text.msdf.MISSING_GLYPH_HEX_ADVANCE¶
0.42
- simvx.core.text.msdf.MISSING_GLYPH_HEX_MIN_DIGIT_PIXELS¶
7.0
- simvx.core.text.msdf.is_default_ignorable(ch: str) bool[source]¶
Whether Unicode defines ch to leave no mark of its own.
- simvx.core.text.msdf.is_invisible_character(ch: str) bool[source]¶
Whether ch is meant to draw nothing, rather than be missing a glyph.
True for the characters that carry no mark of their own: the control, format and space characters, and the ones Unicode defines as default-ignorable. A font having no glyph for one of these is not a missing glyph, so it must not be drawn as a box.
A combining mark is not one of them, deliberately. It carries ink of its own, so a mark that no font on the machine can draw is a missing glyph like any other and is boxed: seeing which codepoint the text carries is what lets a developer fix it, and an unexplained gap is not.
- simvx.core.text.msdf.is_zero_width_character(ch: str) bool[source]¶
Whether ch occupies no horizontal space at all.
The default-ignorable characters, which is every invisible character except the spaces and the control characters. Text measurement and text layout both give one of these no advance, so a heart followed by U+FE0F is exactly as wide as the heart alone and the browser, whose own text engine measures them the same way, agrees.
- simvx.core.text.msdf.missing_glyph_metrics(font, *, hex_form: bool = False) simvx.core.text.font.GlyphMetrics[source]¶
Outline of the box drawn in place of a character no font can supply.
A hollow rectangle standing on the baseline, inset inside the advance that text measurement reserves for an unrenderable character. It is built as an ordinary glyph outline (an outer rectangle, and an inner one wound the other way to punch the hole) so it packs, rasterises and draws through exactly the same path as a real glyph: it therefore takes the colour of the text it stands in and can never come out invisible against its own background.
hex_form returns the narrow box one byte of the codepoint is written inside, two digits one above the other. A missing character is drawn as one of these per byte, so this one’s advance is a fraction of the character’s.
- simvx.core.text.msdf.missing_glyph_hex_digits(ch: str) str[source]¶
The hex digits drawn inside the box for ch, as they are laid out.
Four digits for a codepoint in the Basic Multilingual Plane and six above it, which is the whole range Unicode defines. Five-digit codepoints are written with a leading zero rather than in a ragged five-cell grid: it is the same number, U+01F600 is a spelling of U+1F600 that any lookup accepts, and it leaves only two grid shapes to keep legible instead of three.
- simvx.core.text.msdf.missing_glyph_byte_count(ch: str) int[source]¶
Boxes drawn in place of ch: one per byte of its codepoint, so two or three.
- simvx.core.text.msdf.hex_digit_height(font) float[source]¶
Height the digits in a hex box reach, as a fraction of the em.
Zero when font has no digit to draw them from. It depends on the font alone, which is what lets text measurement and the renderer agree on which form a missing character takes without measurement having to see an atlas.
- simvx.core.text.msdf.writes_out_codepoint(font, em_pixels: float) bool[source]¶
Whether text at em_pixels is large enough to write a codepoint out.
em_pixels is the size the text is laid out at, before any camera or parent transform (see :data:
MISSING_GLYPH_HEX_MIN_DIGIT_PIXELSfor why that, and not the device pixels finally covered, is what the choice is made on).
- simvx.core.text.msdf.missing_glyph_advances(ch: str) tuple[float, float][source]¶
The room ch is reserved in each form, as a fraction of the em.
The plain box’s own advance first, then one hex box per byte of the codepoint. :func:
missing_glyph_advancepicks between them by size; text measurement takes both at once so that it can cache the width of a string without caching it once per size.
- simvx.core.text.msdf.missing_glyph_advance(ch: str, font, em_pixels: float, *, cell_width: float = 0.0) float[source]¶
Room reserved for ch when nothing can draw it, as a fraction of the em.
One hex box per byte of the codepoint where the text is large enough for the digits inside them to be read, and the plain box’s own advance where it is not, so what is reserved is what gets drawn and neither form sits in a gap wider than itself. Text measurement, the layout that draws it and the hit-testing that reads it back all take their width from here, so all three agree; they all decide on the same untransformed em_pixels, so a camera zoom cannot move the boundary under them.
The choice is made on the size alone, not on whether an atlas happens to carry the digits: an atlas that cannot supply them draws the plain box in the room the digits would have had, rather than measuring one width and drawing another.
cell_width is the pitch of the character grid the text sits on, in the same pixels as em_pixels, and is how a caller that has a grid says so: a terminal, a code view, a tabular readout. On a grid a character nothing can draw occupies exactly one cell, whatever form goes in it, so a run of text keeps every later column where the grid puts it and a renderer that places each character in a cell of its own never has a box reaching into the next one. :func:
missing_glyph_cellsfits the form to that cell rather than letting it spill. Text that is not on a grid leaves cell_width at zero and is unaffected.
- class simvx.core.text.msdf.GlyphRegion[source]¶
Atlas region for a packed glyph.
- char: str¶
None
- x: int¶
None
- y: int¶
None
- w: int¶
None
- h: int¶
None
- metrics: simvx.core.text.font.GlyphMetrics¶
None
- u0: float¶
0.0
- v0: float¶
0.0
- u1: float¶
0.0
- v1: float¶
0.0
- class simvx.core.text.msdf.MSDFAtlas(font: simvx.core.text.font.Font, atlas_size: int = 1024, glyph_padding: int = 4, sdf_range: float = 4.0, charset: str | None = None)[source]¶
MSDF font atlas with incremental shelf-based bin packing.
Glyphs are rendered on demand and appended to the atlas. ASCII is pre-seeded at init time so Latin text works without re-uploads.
Initialization
- ensure_glyphs(text: str) bool[source]¶
Ensure all glyphs in text are in the atlas.
Returns True if the atlas was modified (caller should re-upload). Glyphs missing from the underlying font are silently skipped.
- ensure_glyphs_from(chars: str, font: simvx.core.text.font.Font) bool[source]¶
Pack glyphs for chars using an external font into this atlas.
Used by the fallback chain: the primary atlas borrows glyphs from a fallback font so all text renders from a single GPU texture. Returns True if the atlas was modified.
- simvx.core.text.msdf.median_channel(atlas: numpy.ndarray) numpy.ndarray[source]¶
Collapse an MSDF atlas to the single distance field the shader reads.
The MSDF fragment shaders decode a glyph as
median(r, g, b); the three channels only differ within a texel or two of a corner, where the median is what reconstructs the sharp intersection. Evaluating that median per texel yields one channel carrying the same field, at a quarter of the bytes.Storing the result and letting it expand back to
r = g = bon decode (any greyscale image source does this) leaves the shaders untouched: the same trickBitmapAtlasuses to feed plain coverage through the median. The one thing lost is sub-texel corner reconstruction, because the shader now interpolates the median instead of taking the median of interpolated channels. That difference is bounded by the atlas texel size.Args: atlas:
(h, w, 3)or(h, w, 4)uint8 MSDF atlas.Returns:
(h, w)uint8 array.
- class simvx.core.text.msdf.BitmapAtlas(font_path: str, target_size: int, atlas_size: int = 512, charset: str | None = None)[source]¶
Font atlas using FreeType hinted bitmap rendering (no SDF).
Produces pixel-perfect glyphs at a fixed target size. The atlas format is RGBA with R=G=B=coverage so the MSDF shader’s median(r,g,b) acts as a simple alpha blend passthrough.
Initialization
- simvx.core.text.msdf.missing_glyph_cells(ch: str, atlas: simvx.core.text.msdf.MSDFAtlas, em_pixels: float, *, cell_width: float = 0.0) list[tuple[simvx.core.text.msdf.GlyphRegion, float, float, float]][source]¶
Every atlas cell that makes up what is drawn in place of ch.
Each byte of the codepoint in turn, as its box followed by the two digits that write it, top to bottom. One entry per quad, as
(region, x, y, scale): x and y place the quad’s top-left corner relative to the pen position and the baseline, in the atlas font’s own units and including the atlas padding, and scale multiplies the region’s size. A renderer multiplies all three by whatever takes it from atlas units to laid-out pixels, which is the same factor it already uses for an ordinary glyph, and draws each quad in the text colour.em_pixels is the size the text is being laid out at, in the same pixels, and before any camera or parent transform (see
- Data:
MISSING_GLYPH_HEX_MIN_DIGIT_PIXELSfor why that, and not the device pixels finally covered, is what the choice is made on). Where it leaves room for digits of at least that height, the result is the codepoint written out, so a developer can read which character is missing instead of guessing; below it, where the digits would be too small to tell apart, it is a single plain box.
cell_width is the pitch of the character grid the text sits on, in those same pixels. On a grid the character is given exactly one cell, the largest form that fits it is drawn, and it is fitted by scaling the whole form down uniformly rather than by letting it spill: nothing a boxed character draws can reach the column beside it, which matters most where each character is drawn into a cell of its own and the next one lands at a fixed pitch whatever was reserved. Off a grid the scale is 1.0 and the form fills its own advance exactly as it always has.
Either way what is drawn fills the room :func:
missing_glyph_advancereserves for the same em_pixels and cell_width, so measurement and layout never disagree, and it is centred in that room.Empty only when the atlas carries no box at all.
- simvx.core.text.msdf.rasterize_text(text: str, atlas: simvx.core.text.msdf.MSDFAtlas, font_size: int, width: int, height: int, colour: tuple, *, missing_glyph_resolver: collections.abc.Callable[[str], bool] | None = None) numpy.ndarray[source]¶
Rasterize text from an MSDF atlas into an RGBA uint8 image (CPU, backend-agnostic).
Samples the MSDF atlas per-pixel, applies median thresholding, and composites coloured glyphs onto a transparent background. The result is an RGBA array suitable for upload as a texture (e.g. text-on-3D via
create_text_texture, on both the Vulkan and web backends).missing_glyph_resolver answers “no font this backend can reach draws this character”, and is asked only about a character with no atlas cell. It is how the caller’s font fallback chain gets consulted, so a character some other face can draw is left for that face to supply instead of being declared missing and boxed. Without one, the atlas font is the only judge, and a character it lacks is boxed straight away.