// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include #include #include namespace plot::render { namespace { /// Two declared cells agree when neither size differs by more than the atlas' /// tolerance: the test `glyph_is_cell()` makes, for a cell against a cell. bool same_cell(Vec2 a, Vec2 b) noexcept { const auto near = [](f32 p, f32 q) { return std::fabs(p - q) <= kAtlasCellTolerance; }; return near(a.x, b.x) && near(a.y, b.y); } /// `axis` as a unit vector: the cell's local +x, which is what the instance and /// the shader both take (see `TextInstance`). A zero, infinite or NaN direction /// means "unrotated" -- a caller's arithmetic slip must not turn a label into NaN /// geometry, and a basis the shader trusts to be unit must be one. Vec2 unit_axis(Vec2 axis) noexcept { const f32 len = axis.length(); if (std::isfinite(len) && len > 0.0f) { return axis / len; } return Vec2{1.0f, 0.0f}; } } // namespace // ----------------------------------------------------------------- LineBatch void LineBatch::begin() noexcept { v_.clear(); } void LineBatch::clear() noexcept { v_.clear(); } void LineBatch::add_segment(Vec2 a, Vec2 b) { v_.push_back(a.x); v_.push_back(a.y); v_.push_back(b.x); v_.push_back(b.y); } void LineBatch::add_polyline(const Vec2* pts, u32 n) { if (pts == nullptr || n < 2u) { return; // no pair of points, no segment } // Grow once for the whole polyline rather than per segment, so a frame that // stages the same workload as the last one never reallocates. v_.reserve(v_.size() + 4u * (n - 1u)); for (u32 i = 1; i < n; ++i) { add_segment(pts[i - 1u], pts[i]); } } void LineBatch::add_polyline_xy(const f32* xy, u32 n) { if (xy == nullptr || n < 2u) { return; } v_.reserve(v_.size() + 4u * (n - 1u)); for (u32 i = 1; i < n; ++i) { v_.push_back(xy[2u * (i - 1u)]); v_.push_back(xy[2u * (i - 1u) + 1u]); v_.push_back(xy[2u * i]); v_.push_back(xy[2u * i + 1u]); } } // ----------------------------------------------------------------- TextBatch void TextBatch::begin() noexcept { v_.clear(); cell_uv_ = Vec2{}; cell_declared_ = false; } void TextBatch::clear() noexcept { v_.clear(); cell_uv_ = Vec2{}; cell_declared_ = false; } void TextBatch::set_cell(Vec2 cell_uv) noexcept { // One cell per draw (see TextBackend::cell_uv): the draw carries a single // `u_glyph_uv`, so a second, different cell inside one batch would hand it to // glyphs staged for another size. A debug build says so; a release build takes // the last one, as it takes the rest of its state. assert((!cell_declared_ || same_cell(cell_uv_, cell_uv)) && "plot::render::TextBatch: a second, different atlas cell in one batch"); cell_uv_ = cell_uv; cell_declared_ = true; } void TextBatch::add_cell(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 unit_ux, Color color) { if (!cell_declared_) { // Nobody declared a cell: a caller staging raw glyphs, where the glyph's own // rect is the only authority there is. `add_text` and the renderer both // declare one from the backend, so the draw never depends on this. cell_uv_ = Vec2{glyph.u1 - glyph.u0, glyph.v1 - glyph.v0}; cell_declared_ = true; } // A fixed-cell grid (see TextBackend::cell_uv): an instance carries the rect's // *origin*, so a glyph whose rect is not this cell would be drawn at the wrong // size -- and read past its own row of the atlas doing it -- with nothing // downstream able to tell. Loud in a debug build; a release build draws on with // the cell it was told, which is what it did before this check existed. assert(glyph_is_cell(glyph, cell_uv_) && "plot::render::TextBatch: staged glyph's rect is not the atlas cell"); // The cell's own axes: the run direction and its perpendicular, which is the // cell's +v axis -- the one kTextVS builds the quad from. The bearing is in // font pixels, so it scales with the run and follows the same axes: a glyph // keeps its place inside a rotated run instead of drifting along screen +y. const Vec2 uy{-unit_ux.y, unit_ux.x}; const Vec2 corner = pen_px + unit_ux * (glyph.xoff * px) + uy * (glyph.yoff * px); v_.push_back(corner.x); v_.push_back(corner.y); v_.push_back(glyph.u0); v_.push_back(glyph.v0); v_.push_back(unit_ux.x); v_.push_back(unit_ux.y); v_.push_back(color.r); v_.push_back(color.g); v_.push_back(color.b); v_.push_back(color.a); } void TextBatch::add_glyph(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 axis, Color color) { add_cell(pen_px, glyph, px, unit_axis(axis), color); } Vec2 TextBatch::add_text(const char* utf8, Vec2 pen_px, f32 px, const TextBackend& font, Vec2 axis, Color color) { if (utf8 == nullptr) { return pen_px; } // The cell is the backend's, declared once for the whole run: the staged // instances carry origins only, and the draw carries this size (see // TextBackend::cell_uv). set_cell(font.cell_uv()); const Vec2 ux = unit_axis(axis); for (const char* p = utf8; *p != '\0';) { // The backend's own codepoints and the backend's own advances -- the same // numbers `measure()` sums -- scaled by `px` and measured along the basis, // so a run staged here is exactly as long as the plot layer measured it and // ends where the space reserved for the label ends. const Glyph* glyph = font.glyph(next_codepoint(p)); if (glyph == nullptr) { continue; // no artwork for this codepoint: nothing to draw, nothing to advance } add_cell(pen_px, *glyph, px, ux, color); pen_px = pen_px + ux * (glyph->advance * px); } return pen_px; } } // namespace plot::render