Files
plotter/src/render/primitives.cpp
T
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

155 lines
5.5 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include <plot/render/primitives.hpp>
#include <cassert>
#include <cmath>
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) {
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);
}
void TextBatch::add_glyph(Vec2 pen_px, const Glyph& glyph, f32 px, Vec2 axis) {
add_cell(pen_px, glyph, px, unit_axis(axis));
}
Vec2 TextBatch::add_text(const char* utf8, Vec2 pen_px, f32 px, const TextBackend& font,
Vec2 axis) {
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);
pen_px = pen_px + ux * (glyph->advance * px);
}
return pen_px;
}
} // namespace plot::render