Files
Martino Ferrari ed3a8ce645 licence: MIT LICENSE and SPDX identifiers in every first-party file
The repository had no licence at all, so every source was all-rights-reserved
by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino
Ferrari) and a two-line SPDX header to each of the 113 first-party sources --
48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the
grant travels with each file rather than relying on the root document alone.

Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely
vendored code is untouched: third_party/stb_truetype.h carries no header of
ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py
emit() now writes the two lines into its header comment and the file was
regenerated -- gen_atlas.py --check passes and the regeneration adds only those
comment lines to the previously committed file.

README's provenance section now states MIT and links LICENSE.

No code, test or public API change.
2026-09-28 22:13:40 +02:00

179 lines
9.0 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <cmath>
#include <plot/core/types.hpp>
#include <plot/math/vec2.hpp>
namespace plot::render {
/// 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.
///
/// **The rect is the atlas cell**, not the ink box: every glyph of a backend
/// occupies one cell of one size, so `u1 - u0` and `v1 - v0` are the same pair for
/// every codepoint -- the pair `TextBackend::cell_uv()` reports. The ink sits
/// inside the cell at the bearing. See `cell_uv()` for why the pipeline needs
/// that, and `glyph_is_cell()` for the check that enforces it.
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;
/// Tolerance for comparing two normalised atlas sizes. A backend derives a rect
/// size as `u1 - u0`, a subtraction of two rounded fractions, so a *correct* cell
/// comes back a few 1e-8 out; the smallest rect that is genuinely a different
/// size differs by one atlas texel, which is 1e-4 or coarser on any atlas a GPU
/// will hold. 1e-6 separates "the same cell, rounded twice" from "a different
/// rect" with orders of magnitude to spare on both sides.
inline constexpr f32 kAtlasCellTolerance = 1e-6f;
/// True when `glyph`'s rect is the atlas cell `cell_uv` -- the invariant an atlas
/// is required to keep (see `TextBackend::cell_uv`).
///
/// It is a function rather than an assertion inside one caller because it is the
/// check the staging path asserts for every glyph it stages, and a test has to be
/// able to drive it over a backend that breaks it without aborting the suite.
inline bool glyph_is_cell(const Glyph& glyph, Vec2 cell_uv) noexcept {
const auto near = [](f32 a, f32 b) { return std::fabs(a - b) <= kAtlasCellTolerance; };
return near(glyph.u1 - glyph.u0, cell_uv.x) && near(glyph.v1 - glyph.v0, cell_uv.y);
}
/// 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**: the atlas is a *fixed-cell grid* and
/// `cell_uv()` is where that size is declared -- see it for the full contract
/// and for what a proportional face has to do to satisfy it. The staging path
/// asserts `glyph_is_cell()` for every glyph it stages in a debug build.
class TextBackend {
public:
TextBackend() noexcept = default;
TextBackend(const TextBackend&) = default;
TextBackend& operator=(const TextBackend&) = default;
virtual ~TextBackend() = default;
/// 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.
virtual f32 height() const = 0;
/// 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 face's em size in font pixels: the size the atlas was baked at, in the
/// same units as `height()` and `ascent()`.
///
/// It exists so a run can be drawn at the size a caller asked for whatever the
/// backend was baked at: the renderer scales by `requested_px / em_px()`, so a
/// backend re-baked at another size -- or swapped for a different face -- keeps
/// drawing `Style::axis_font_px`, and the renderer never has to know which font
///
/// A backend that *wraps* another to draw it at a size scales this with the
/// rest of its metrics -- it is in the wrapper's units, like `height()` -- so a
/// caller that reads the backend back and draws at `requested_px / em_px()`
/// gets a ratio of two screen-pixel quantities and cannot scale twice.
virtual f32 em_px() 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.
///
/// Its rect is the atlas **cell**, not the ink box: see `cell_uv()`.
virtual const Glyph* glyph(u32 codepoint) const = 0;
/// The atlas size of one cell, as a normalised rect size: `(u1 - u0, v1 - v0)`
/// of every glyph the atlas holds -- the size `glyph()` reports each of them
/// with. `(0, 0)` is legal and means "no text".
///
/// **An atlas is a fixed-cell grid, and this is where that is declared.** Each
/// glyph is drawn in a cell of this one size, because the pipeline stages a
/// glyph's rect *origin* per instance and carries the rect's *size* once for
/// the whole draw: six floats per glyph instead of eight, no per-glyph size in
/// the vertex shader, and rotation for two of those floats. The staging path
/// asserts `glyph_is_cell()` -- every staged glyph's rect against this value --
/// in a debug build, so a backend that breaks the contract fails loudly rather
/// than drawing every glyph at whichever size it happened to be told.
///
/// **For implementers:** a proportional face therefore draws its glyphs *into*
/// a uniform cell -- the ink placed inside it at `xoff`/`yoff`, the pen
/// advanced by `advance` -- rather than trimming each atlas rect to its ink,
/// and spends the atlas area instead of the correctness. The cell has to be
/// wide enough for the widest advance and tall enough for the tallest ink, and
/// this must agree with the rects `glyph()` reports; a backend whose rects are
/// ink boxes (a naive TrueType packing, which trims each bitmap to its ink)
/// violates it.
virtual Vec2 cell_uv() 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".
/// The bytes are a grid of `cell_uv()`-sized cells.
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