refactor(render): TextBackend owns its atlas, and staged glyphs carry a rotation basis
This commit is contained in:
1 parent
90e17a0a11
commit
e074f57ddf
23 files changed
+1073
-227
No files matched your search
@@ -3,13 +3,70 @@
|
||||
|
||||
namespace plot::render {
|
||||
|
||||
/// Text metrics the plot layer lays axes and labels out with. It is a pure
|
||||
/// interface so the layout code never depends on the bitmap font, and so a
|
||||
/// caller can measure with a host toolkit's font instead.
|
||||
/// One glyph's atlas rectangle and metrics, in font pixels.
|
||||
///
|
||||
/// **The rectangle is normalised, v grows downward.** `(u0, v0)` is the glyph's
|
||||
/// top-left atlas texel and `(u1, v1)` its bottom-right, each divided by the
|
||||
/// atlas' size -- so the atlas is stored with its top row first, exactly the row
|
||||
/// order `atlas_pixels()` hands over, and a larger `v` is a row further down.
|
||||
/// Text is laid out with the same handedness: a glyph's own +y runs from `v0`
|
||||
/// toward `v1`, which on screen is downward when the glyph is unrotated. A
|
||||
/// backend that rasterised its atlas bottom row first would draw every glyph
|
||||
/// mirrored unless it divided `v` the other way, so the convention is stated
|
||||
/// once, here, and every consumer -- the text shader's UVs, the vertex shader's
|
||||
/// second cell axis, `TextBatch` -- uses it and nothing else.
|
||||
///
|
||||
/// `(xoff, yoff)` is the bearing from the pen to the glyph's top-left corner
|
||||
/// (top-left origin, font pixels) and `advance` is how far the pen moves along
|
||||
/// the baseline. Both are in font pixels, so a caller scaling a run multiplies
|
||||
/// them by the same factor it draws the atlas with.
|
||||
struct Glyph {
|
||||
f32 u0 = 0.0f;
|
||||
f32 v0 = 0.0f;
|
||||
f32 u1 = 0.0f;
|
||||
f32 v1 = 0.0f;
|
||||
f32 xoff = 0.0f;
|
||||
f32 yoff = 0.0f;
|
||||
f32 advance = 0.0f;
|
||||
};
|
||||
|
||||
/// A byte sequence that is not valid UTF-8, as reported by `next_codepoint`. It
|
||||
/// is deliberately not a Unicode scalar value, so it can only map to whatever a
|
||||
/// backend draws for "no glyph" rather than colliding with a real codepoint.
|
||||
inline constexpr u32 kInvalidCodepoint = 0xFFFFFFFFu;
|
||||
|
||||
/// Decodes one UTF-8 sequence from `p` and advances `p` past it -- one byte when
|
||||
/// the sequence is malformed, so a damaged label still terminates.
|
||||
///
|
||||
/// It belongs to this interface rather than to a font because it is what keeps a
|
||||
/// backend's `measure()` and the glyphs the pipeline stages for the same string
|
||||
/// in step: both walk the string with this decoder, so neither can disagree with
|
||||
/// the other about where a codepoint starts.
|
||||
u32 next_codepoint(const char*& p) noexcept;
|
||||
|
||||
/// The font a `Plot` measures with and the renderer draws with: metrics, plus the
|
||||
/// coverage atlas the text pipeline samples.
|
||||
///
|
||||
/// Text is passed as UTF-8, not as a byte-per-character string: `plot::format_si`
|
||||
/// emits U+00B5, U+00B0 and U+03A9 for micro, degree and ohm, and an
|
||||
/// implementation that ignores that prints mojibake.
|
||||
///
|
||||
/// The atlas is the backend's own: it owns the pixels, the glyph table that
|
||||
/// describes them and the `revision()` that says when they changed. The renderer
|
||||
/// uploads whatever the backend exposes and re-uploads when the revision moves,
|
||||
/// so a backend can be swapped -- or re-baked at another size -- without the
|
||||
/// renderer knowing anything about fonts. `measure()`/`height()`/`ascent()` are
|
||||
/// in the atlas' font pixels: the pipeline scales them, never re-rasterises.
|
||||
///
|
||||
/// **One atlas, one cell size.** A staged glyph carries the *origin* of its rect
|
||||
/// and the draw carries the rect's size, once, for every instance in it (see
|
||||
/// `TextProgram::set_glyph_uv`) -- which is what keeps a glyph at six floats while
|
||||
/// still letting it be rotated. So every glyph a backend publishes must occupy a
|
||||
/// rect of the same size: its cell, with the ink inside it at `(xoff, yoff)`. A
|
||||
/// proportional face therefore bakes each glyph into a cell wide enough for the
|
||||
/// widest advance and spends the atlas area rather than the correctness; a
|
||||
/// backend whose rects differ in size would have its glyphs read through
|
||||
/// whichever rect was staged first.
|
||||
class TextBackend {
|
||||
public:
|
||||
TextBackend() noexcept = default;
|
||||
@@ -17,8 +74,8 @@ class TextBackend {
|
||||
TextBackend& operator=(const TextBackend&) = default;
|
||||
virtual ~TextBackend() = default;
|
||||
|
||||
/// Width of `utf8` in the same units as `height()` (pixels for the bitmap
|
||||
/// font). Zero for the empty string.
|
||||
/// Width of `utf8` in the same units as `height()` (font pixels for a font
|
||||
/// atlas). Zero for the empty string.
|
||||
virtual f32 measure(const char* utf8) const = 0;
|
||||
|
||||
/// Line height: the cell a single line of text occupies.
|
||||
@@ -27,6 +84,29 @@ class TextBackend {
|
||||
/// Distance from the top of the cell to the baseline, so a caller can align
|
||||
/// text bottoms instead of cell bottoms.
|
||||
virtual f32 ascent() const = 0;
|
||||
|
||||
/// The glyph for `codepoint`, or null when this backend has none -- the caller
|
||||
/// then draws nothing for it (a backend that wants a visible missing-glyph box
|
||||
/// publishes one as the glyph for the codepoints it cannot render). The
|
||||
/// returned pointer stays valid for the backend's lifetime, so callers may hold
|
||||
/// it across a frame.
|
||||
virtual const Glyph* glyph(u32 codepoint) const = 0;
|
||||
|
||||
/// 8-bit single-channel coverage, row-major, `atlas_width() * atlas_height()`
|
||||
/// bytes, top row first. Uploaded as GL_R8. Null is legal and means "no text".
|
||||
virtual const u8* atlas_pixels() const = 0;
|
||||
|
||||
/// Atlas size in texels. Zero width or height means "no text", like a null
|
||||
/// `atlas_pixels()`.
|
||||
virtual u32 atlas_width() const = 0;
|
||||
virtual u32 atlas_height() const = 0;
|
||||
|
||||
/// Bumped whenever `atlas_pixels()` content changes, so the renderer knows to
|
||||
/// re-upload instead of trusting its cache. Must not change otherwise -- a
|
||||
/// revision that moves on its own would re-upload the same bytes every frame --
|
||||
/// and any change counts, so a backend may re-bake at a new size and keep
|
||||
/// counting up.
|
||||
virtual u32 revision() const = 0;
|
||||
};
|
||||
|
||||
} // namespace plot::render
|
||||
Reference in new issue
Block a user