fix(render): environment-aware atlas drift guard, pinned text scale, shipped-atlas coverage
This commit is contained in:
1 parent
5ca3587325
commit
1cb5970757
6 files changed
+257
-31
No files matched your search
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <plot/core/types.hpp>
|
||||||
|
|
||||||
|
namespace plot::render::detail {
|
||||||
|
|
||||||
|
/// The factor a text run is drawn by: `requested_px / em_px`, a plain ratio.
|
||||||
|
///
|
||||||
|
/// It is a ratio rather than a whole multiple because the renderer's built-in
|
||||||
|
/// face is anti-aliased: a style asking for 12 px from a 13 px bake is drawn at
|
||||||
|
/// 12/13, where a 1-bit glyph would have had to snap to the nearest multiple of
|
||||||
|
/// its cell. Everything the renderer stages is scaled by this one factor, so the
|
||||||
|
/// labels a plot laid out and the glyphs the pipeline draws cannot disagree.
|
||||||
|
///
|
||||||
|
/// A backend that cannot state an em -- `em_px()` at or below zero, or NaN --
|
||||||
|
/// yields a 1:1 scale rather than dividing by it or propagating a NaN into every
|
||||||
|
/// vertex. The requested size being NaN, zero or negative does the same. The rule
|
||||||
|
/// lives here, not inline in the renderer, so it is one function and can be
|
||||||
|
/// asserted without a GL context; it is instrumentation-free and part of no
|
||||||
|
/// public surface (`*/detail/` headers stay out of the umbrella).
|
||||||
|
inline f32 text_scale(f32 requested_px, f32 em_px) noexcept {
|
||||||
|
const f32 ratio = requested_px / em_px;
|
||||||
|
return (em_px > 0.0f && ratio > 0.0f) ? ratio : 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace plot::render::detail
|
||||||
@@ -115,9 +115,11 @@ class TextBackend {
|
|||||||
/// backend was baked at: the renderer scales by `requested_px / em_px()`, so a
|
/// 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
|
/// 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
|
/// drawing `Style::axis_font_px`, and the renderer never has to know which font
|
||||||
/// it holds. A backend with no meaningful em (one whose "font pixels" are an
|
///
|
||||||
/// arbitrary unit) returns something proportional, `height()` being the
|
/// A backend that *wraps* another to draw it at a size scales this with the
|
||||||
/// natural choice.
|
/// 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;
|
virtual f32 em_px() const = 0;
|
||||||
|
|
||||||
/// The glyph for `codepoint`, or null when this backend has none -- the caller
|
/// The glyph for `codepoint`, or null when this backend has none -- the caller
|
||||||
|
|||||||
+13
-12
@@ -3,6 +3,7 @@
|
|||||||
#include <plot/gl/api.hpp>
|
#include <plot/gl/api.hpp>
|
||||||
#include <plot/gl/caps.hpp>
|
#include <plot/gl/caps.hpp>
|
||||||
#include <plot/gl/loader.hpp>
|
#include <plot/gl/loader.hpp>
|
||||||
|
#include <plot/render/detail/text_scale.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -97,15 +98,12 @@ void warn_replaced_text_backend(const render::TextBackend* previous) noexcept {
|
|||||||
|
|
||||||
void Renderer::AxisFont::reset(const render::TextBackend& font, f32 requested_px) noexcept {
|
void Renderer::AxisFont::reset(const render::TextBackend& font, f32 requested_px) noexcept {
|
||||||
font_ = &font;
|
font_ = &font;
|
||||||
// The requested size is an em size, exactly as `em_px()` reports the size the
|
// An anti-aliased face is legible at any ratio, so a style asking for 12 px
|
||||||
// atlas was baked at, so the ratio between them is all the scaling there is:
|
// from a 13 px bake is drawn at 12/13 rather than snapped to a whole multiple
|
||||||
// an anti-aliased face is legible at any ratio, and a style asking for 12 px
|
// of the cell. `detail::text_scale` owns that rule, including the 1:1 fallback
|
||||||
// from a 13 px bake gets 12/13 rather than a snapped whole multiple. A backend
|
// for a backend that cannot state an em -- it is one function, and the test
|
||||||
// that cannot say (`em_px()` at or below zero, or NaN) keeps a 1:1 scale rather
|
// suite asserts it without a GL context.
|
||||||
// than dividing by it.
|
scale_ = render::detail::text_scale(requested_px, font.em_px());
|
||||||
const f32 baked = font.em_px();
|
|
||||||
const f32 ratio = requested_px / baked;
|
|
||||||
scale_ = (baked > 0.0f && ratio > 0.0f) ? ratio : 1.0f;
|
|
||||||
cell_px_ = font.height() * scale_;
|
cell_px_ = font.height() * scale_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +120,12 @@ f32 Renderer::AxisFont::ascent() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f32 Renderer::AxisFont::em_px() const {
|
f32 Renderer::AxisFont::em_px() const {
|
||||||
// The em is the one metric that does not scale: it is the face's own unit, and
|
// Scaled with the rest of the metrics, not forwarded raw: `TextBackend::em_px()`
|
||||||
// it is what the scale above was computed from.
|
// is documented to be in the same units as `height()` and `ascent()`, and this
|
||||||
return font_ != nullptr ? font_->em_px() : 0.0f;
|
// wrapper's are all screen pixels. A caller that reads the backend back off the
|
||||||
|
// plot and draws at `requested_px / em_px()` therefore gets 1:1 rather than
|
||||||
|
// scaling a second time (see the base-class contract).
|
||||||
|
return font_ != nullptr ? font_->em_px() * scale_ : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
const render::Glyph* Renderer::AxisFont::glyph(u32 codepoint) const {
|
const render::Glyph* Renderer::AxisFont::glyph(u32 codepoint) const {
|
||||||
|
|||||||
+153
-15
@@ -3,12 +3,14 @@
|
|||||||
// Nothing here touches GL, so it runs on any machine that can build the tests.
|
// Nothing here touches GL, so it runs on any machine that can build the tests.
|
||||||
#include "harness.hpp"
|
#include "harness.hpp"
|
||||||
|
|
||||||
|
#include <plot/render/detail/text_scale.hpp>
|
||||||
#include <plot/render/embedded_mono_font.hpp>
|
#include <plot/render/embedded_mono_font.hpp>
|
||||||
#include <plot/render/primitives.hpp>
|
#include <plot/render/primitives.hpp>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <limits>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -221,6 +223,83 @@ PLOT_TEST(embedded_atlas_covers_every_expected_codepoint) {
|
|||||||
PLOT_CHECK(f.glyph(0x4E00u)->advance > 0.0f);
|
PLOT_CHECK(f.glyph(0x4E00u)->advance > 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The *shipped table* -- not the generator, and not a lookup -- is what the
|
||||||
|
// renderer uploads, so the coverage and quality contract is asserted on it
|
||||||
|
// directly rather than only on the codepoint lookups above. Slot by slot, the
|
||||||
|
// atlas holds exactly the documented codepoints in order, every cell is the one
|
||||||
|
// declared size and lies inside the image, every cell but the space carries ink,
|
||||||
|
// and the fallback box is a real inked glyph at its fixed final slot.
|
||||||
|
//
|
||||||
|
// It is the case the drift guard beside it cannot replace: `--check` compares the
|
||||||
|
// committed bytes to a bake made with whatever coverage list the generator
|
||||||
|
// currently holds, so dropping a codepoint from that list keeps it green. These
|
||||||
|
// assertions are what make a dropped codepoint (say U+00B5) or a blank cell fail
|
||||||
|
// loudly, on the data that actually ships.
|
||||||
|
PLOT_TEST(embedded_atlas_matches_its_documented_coverage) {
|
||||||
|
const EmbeddedMonoFont f;
|
||||||
|
const plot::Vec2 cell = f.cell_uv();
|
||||||
|
|
||||||
|
// The documented coverage set, in the documented slot order: printable ASCII,
|
||||||
|
// the three codepoints `plot::format_si` emits plus the two label dashes, then
|
||||||
|
// the fallback box appended last.
|
||||||
|
std::vector<plot::u32> documented;
|
||||||
|
for (plot::u32 cp = 0x20u; cp <= 0x7Eu; ++cp) documented.push_back(cp);
|
||||||
|
for (plot::u32 cp : {0x00B0u, 0x00B5u, 0x03A9u, 0x2013u, 0x2014u}) documented.push_back(cp);
|
||||||
|
PLOT_CHECK(EmbeddedMonoFont::glyph_count() == documented.size() + 1u);
|
||||||
|
|
||||||
|
const plot::u8* atlas = f.atlas_pixels();
|
||||||
|
const plot::u32 atlas_w = f.atlas_width();
|
||||||
|
const plot::u32 atlas_h = f.atlas_height();
|
||||||
|
const std::size_t atlas_texels = static_cast<std::size_t>(atlas_w) * atlas_h;
|
||||||
|
plot::u32 inked_cells = 0u;
|
||||||
|
|
||||||
|
for (plot::u32 slot = 0; slot < EmbeddedMonoFont::glyph_count(); ++slot) {
|
||||||
|
const plot::u32 expected =
|
||||||
|
slot < documented.size() ? documented[slot] : EmbeddedMonoFont::kFallbackCodepoint;
|
||||||
|
const plot::u32 cp = EmbeddedMonoFont::codepoint_of_slot(slot);
|
||||||
|
PLOT_CHECK(cp == expected); // the table is the documented one, in order
|
||||||
|
if (cp != EmbeddedMonoFont::kFallbackCodepoint) {
|
||||||
|
PLOT_CHECK(EmbeddedMonoFont::glyph_index(cp) == slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Its rect is the one declared cell, and it lies inside the image ...
|
||||||
|
const plot::render::Glyph* g = f.glyph(cp);
|
||||||
|
PLOT_CHECK(g != nullptr);
|
||||||
|
PLOT_CHECK(plot::render::glyph_is_cell(*g, cell));
|
||||||
|
PLOT_CHECK(g->u0 >= 0.0f && g->u1 <= 1.0f && g->v0 >= 0.0f && g->v1 <= 1.0f);
|
||||||
|
|
||||||
|
// ... and the cell's texels are all inside the image with ink in them, bar
|
||||||
|
// the one documented blank. The bounds check is what a table claiming more
|
||||||
|
// bytes than it ships fails on.
|
||||||
|
const plot::Vec2 origin = EmbeddedMonoFont::atlas_origin(slot);
|
||||||
|
const plot::u32 ox = static_cast<plot::u32>(origin.x);
|
||||||
|
const plot::u32 oy = static_cast<plot::u32>(origin.y);
|
||||||
|
PLOT_CHECK(ox + EmbeddedMonoFont::cell_width() <= atlas_w);
|
||||||
|
PLOT_CHECK(oy + EmbeddedMonoFont::cell_height() <= atlas_h);
|
||||||
|
plot::u32 ink = 0u;
|
||||||
|
for (plot::u32 y = 0; y < EmbeddedMonoFont::cell_height(); ++y) {
|
||||||
|
for (plot::u32 x = 0; x < EmbeddedMonoFont::cell_width(); ++x) {
|
||||||
|
const std::size_t index = static_cast<std::size_t>(oy + y) * atlas_w + (ox + x);
|
||||||
|
PLOT_CHECK(index < atlas_texels);
|
||||||
|
ink += atlas[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cp == 0x20u) {
|
||||||
|
PLOT_CHECK(ink == 0u); // the space is the one blank cell
|
||||||
|
} else {
|
||||||
|
PLOT_CHECK(ink > 0u); // every covered glyph, and the box, has ink
|
||||||
|
++inked_cells;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PLOT_CHECK(inked_cells == EmbeddedMonoFont::glyph_count() - 1u);
|
||||||
|
|
||||||
|
// The fallback sits at the fixed final slot, as documented: a regeneration that
|
||||||
|
// inserted or dropped a glyph would move it, and this catches that where the
|
||||||
|
// lookups alone would not.
|
||||||
|
PLOT_CHECK(fallback_slot() == EmbeddedMonoFont::glyph_count() - 1u);
|
||||||
|
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x10FFFFu) == fallback_slot());
|
||||||
|
}
|
||||||
|
|
||||||
// The atlas carries more than one coverage value: an anti-aliased raster has grey
|
// The atlas carries more than one coverage value: an anti-aliased raster has grey
|
||||||
// edges, and a 1-bit table could only ever be 0 or 255. This is the data-level
|
// edges, and a 1-bit table could only ever be 0 or 255. This is the data-level
|
||||||
// half of the anti-aliasing evidence (the frame-level half is in the render
|
// half of the anti-aliasing evidence (the frame-level half is in the render
|
||||||
@@ -363,27 +442,86 @@ PLOT_TEST(embedded_atlas_is_uploadable) {
|
|||||||
static_cast<plot::f32>(EmbeddedMonoFont::cell_width()), 1e-9);
|
static_cast<plot::f32>(EmbeddedMonoFont::cell_width()), 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The committed atlas is the generator's output, not a hand edit, and the
|
// The size the renderer draws a run by is a plain ratio, `requested_px / em_px`,
|
||||||
// generator is deterministic: the same source font, em size and supersample
|
// and not a whole multiple of the backend's cell. This is the rule the whole
|
||||||
// factor produce it byte for byte. The check runs the committed script against
|
// anti-aliasing change rests on: a style asking for 12 px from a 13 px bake is
|
||||||
// the committed file, so a table touched by hand, a stale bake after a coverage
|
// drawn at 12/13, where the old 1-bit font had to snap to a multiple of its 8 px
|
||||||
// change, or a non-deterministic generator all fail here.
|
// cell (16 px here). `detail::text_scale` is that rule, so it is asserted without
|
||||||
//
|
// a GL context; the frame-level half is in the render smoke test.
|
||||||
// `fontTools` is an authoring-time tool and the test machine is not required to
|
PLOT_TEST(text_scale_is_a_ratio_not_a_whole_multiple) {
|
||||||
// have it. Its absence is *reported*, not silently passed: the generator exits
|
using plot::render::detail::text_scale;
|
||||||
// with a distinct code for it, and this case says so on stdout.
|
const plot::f32 em = 13.0f; // what the committed atlas is baked at
|
||||||
|
|
||||||
|
// 12/13 = 0.9231, so the 17-px cell is drawn 15.69 px tall -- not the 16 px the
|
||||||
|
// old rule would have chosen, and not the 17 px of a whole multiple of it.
|
||||||
|
PLOT_CHECK_NEAR(text_scale(12.0f, em), 12.0f / 13.0f, 1e-6);
|
||||||
|
PLOT_CHECK(std::fabs(text_scale(12.0f, em) * 17.0f - 16.0f) > 0.2f);
|
||||||
|
PLOT_CHECK(std::fabs(text_scale(12.0f, em) * 17.0f - 17.0f) > 0.2f);
|
||||||
|
|
||||||
|
// A ratio that is not a multiple is not rounded to one ...
|
||||||
|
PLOT_CHECK_NEAR(text_scale(20.0f, em), 20.0f / 13.0f, 1e-6);
|
||||||
|
PLOT_CHECK(text_scale(20.0f, em) > 1.5f && text_scale(20.0f, em) < 1.6f);
|
||||||
|
|
||||||
|
// ... and an exact multiple is exactly that multiple, so nothing is lost when
|
||||||
|
// the request does line up with the bake.
|
||||||
|
PLOT_CHECK_NEAR(text_scale(13.0f, em), 1.0f, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(text_scale(26.0f, em), 2.0f, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(text_scale(39.0f, em), 3.0f, 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A backend that cannot state an em must not take the frame with it: the scale
|
||||||
|
// falls back to 1:1 rather than dividing by zero or propagating a NaN into every
|
||||||
|
// vertex, and the same holds for a nonsensical request. No shipped backend can
|
||||||
|
// reach this -- `EmbeddedMonoFont::em_px()` is a positive constant -- which is
|
||||||
|
// exactly why the rule is a function with its own case instead of a bare
|
||||||
|
// division inside `Renderer::AxisFont::reset`.
|
||||||
|
PLOT_TEST(text_scale_guards_a_backend_without_an_em) {
|
||||||
|
using plot::render::detail::text_scale;
|
||||||
|
const plot::f32 nan = std::numeric_limits<plot::f32>::quiet_NaN();
|
||||||
|
|
||||||
|
PLOT_CHECK_NEAR(text_scale(12.0f, 0.0f), 1.0f, 1e-6); // no em at all
|
||||||
|
PLOT_CHECK_NEAR(text_scale(12.0f, -13.0f), 1.0f, 1e-6); // nonsense em
|
||||||
|
PLOT_CHECK_NEAR(text_scale(12.0f, nan), 1.0f, 1e-6); // NaN em
|
||||||
|
PLOT_CHECK_NEAR(text_scale(nan, 13.0f), 1.0f, 1e-6); // NaN request
|
||||||
|
PLOT_CHECK_NEAR(text_scale(0.0f, 13.0f), 1.0f, 1e-6); // zero request
|
||||||
|
PLOT_CHECK_NEAR(text_scale(-12.0f, 13.0f), 1.0f, 1e-6); // negative request
|
||||||
|
|
||||||
|
// The guard does not fire for a healthy pair: the ratio is still exact.
|
||||||
|
PLOT_CHECK_NEAR(text_scale(26.0f, 13.0f), 2.0f, 1e-6);
|
||||||
|
PLOT_CHECK_NEAR(text_scale(12.0f, 13.0f), 12.0f / 13.0f, 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Three exit codes say something about the *machine* rather than about the
|
||||||
|
// committed bytes, and none of them can tell a drifted table from a good one:
|
||||||
|
// `gen_atlas.py`'s EXIT_NO_FONTTOOLS (fontTools not installed, 3) and
|
||||||
|
// EXIT_BAD_FONT (no source face -- the adwaita-fonts package -- or an
|
||||||
|
// unparseable one, 2), and the shell's "command not found" for a missing
|
||||||
|
// `python3` (127). Each is *reported* with what is missing and skipped, rather
|
||||||
|
// than failed, so an environment difference does not look like atlas drift --
|
||||||
|
// a machine with fontTools from pip but no distro font package would otherwise
|
||||||
|
// fail here and send someone hunting for a corruption that is not there. Only a
|
||||||
|
// real mismatch between the committed atlas and a fresh bake (1) fails. The
|
||||||
|
// data cases above still assert the shipped table on such a machine, so the
|
||||||
|
// only thing lost is the drift comparison itself.
|
||||||
PLOT_TEST(generator_is_deterministic_and_current) {
|
PLOT_TEST(generator_is_deterministic_and_current) {
|
||||||
const std::string command = std::string("python3 ") + kSourceDir +
|
// Both paths are quoted: `kSourceDir` is a build-time value, and a source tree
|
||||||
"/tools/fonts/gen_atlas.py --check --out " + kSourceDir +
|
// whose path contains a space would otherwise split into extra arguments.
|
||||||
"/src/render/embedded_mono_font.cpp";
|
const std::string command = "python3 \"" + std::string(kSourceDir) +
|
||||||
|
"/tools/fonts/gen_atlas.py\" --check --out \"" + kSourceDir +
|
||||||
|
"/src/render/embedded_mono_font.cpp\"";
|
||||||
const int status = std::system(command.c_str());
|
const int status = std::system(command.c_str());
|
||||||
PLOT_CHECK(status != -1);
|
PLOT_CHECK(status != -1);
|
||||||
PLOT_CHECK(WIFEXITED(status));
|
PLOT_CHECK(WIFEXITED(status));
|
||||||
const int code = WEXITSTATUS(status);
|
const int code = WEXITSTATUS(status);
|
||||||
if (code == 3) { // `gen_atlas.py`'s "fontTools is not installed" code
|
const char* missing = code == 3 ? "fontTools is not installed"
|
||||||
|
: code == 2 ? "the Adwaita Mono face is not installed (adwaita-fonts)"
|
||||||
|
: code == 127 ? "python3 is not on PATH"
|
||||||
|
: nullptr;
|
||||||
|
if (missing != nullptr) {
|
||||||
std::printf(
|
std::printf(
|
||||||
"SKIPPED: generator_is_deterministic_and_current -- fontTools is not "
|
"SKIPPED: generator_is_deterministic_and_current -- %s, so the committed "
|
||||||
"installed, so the atlas could not be re-baked\n");
|
"atlas could not be re-baked\n",
|
||||||
|
missing);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PLOT_CHECK(code == 0);
|
PLOT_CHECK(code == 0);
|
||||||
|
|||||||
@@ -426,6 +426,61 @@ class CountFenceWaits {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// The renderer draws a run at the size the style asked for, not at a whole
|
||||||
|
// multiple of the backend's cell, and it reports what it drew. `axis_font_px()`
|
||||||
|
// is that report, and the committed atlas is baked at 13 px, so the default
|
||||||
|
// style's 12 px is a 12/13 scale -- exactly the case the old rule snapped to
|
||||||
|
// 16 px. No other case sets a size other than the default, and the ones that
|
||||||
|
// compare a measure compare values scaled by the same factor, so they hold under
|
||||||
|
// a snapping rule too; this is the one that does not.
|
||||||
|
PLOT_TEST(axis_text_is_drawn_at_the_requested_em_not_a_whole_multiple) {
|
||||||
|
plottest::HeadlessContext ctx(kW, kH);
|
||||||
|
plot::Status st;
|
||||||
|
PLOT_CHECK(ctx.create(&st));
|
||||||
|
PLOT_CHECK(plot::gl::load(nullptr));
|
||||||
|
|
||||||
|
// The renderer's own backend, for the raw metrics the ratio is taken against.
|
||||||
|
const plot::render::EmbeddedMonoFont font;
|
||||||
|
const plot::f32 baked = font.em_px();
|
||||||
|
const plot::f32 cell = font.height();
|
||||||
|
|
||||||
|
plot::Renderer r;
|
||||||
|
r.style().axis_font_px = 12.0f; // the default style's em, against a 13 px bake
|
||||||
|
PLOT_CHECK(r.init(&st));
|
||||||
|
PLOT_CHECK_NEAR(r.axis_font_px(), cell * (12.0f / baked), 1e-3);
|
||||||
|
PLOT_CHECK(std::fabs(r.axis_font_px() - 16.0f) > 0.2f); // 2 x the old 8 px glyph
|
||||||
|
PLOT_CHECK(std::fabs(r.axis_font_px() - 17.0f) > 0.2f); // 1 x this cell
|
||||||
|
|
||||||
|
plot::Plot p;
|
||||||
|
p.set_viewport({0.0f, 0.0f, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||||
|
p.add_x_axis({"t", {0.0, 1000.0}, true, plot::AxisPlace::Bottom, false});
|
||||||
|
p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||||
|
const plot::Rect viewport{0.0f, 0.0f, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)};
|
||||||
|
r.begin_frame(kW, kH, viewport);
|
||||||
|
r.draw(p);
|
||||||
|
r.end_frame();
|
||||||
|
|
||||||
|
// The plot lays out with the backend the renderer installed, so the size it
|
||||||
|
// measured with is the size it drew with.
|
||||||
|
const plot::render::TextBackend* backend = p.text_backend();
|
||||||
|
PLOT_CHECK(backend != nullptr);
|
||||||
|
PLOT_CHECK_NEAR(backend->em_px(), 12.0f, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(backend->height(), cell * (12.0f / baked), 1e-3);
|
||||||
|
// `em_px()` is in the wrapper's units, like `height()`: a caller drawing at
|
||||||
|
// `requested_px / em_px()` reads 12/12 = 1 and cannot scale a second time.
|
||||||
|
PLOT_CHECK_NEAR(backend->em_px() / backend->height(), baked / cell, 1e-6);
|
||||||
|
|
||||||
|
// An exact multiple still lands exactly, and the frame re-reads the style.
|
||||||
|
r.style().axis_font_px = 26.0f; // 2 x 13
|
||||||
|
r.begin_frame(kW, kH, viewport);
|
||||||
|
r.draw(p);
|
||||||
|
r.end_frame();
|
||||||
|
PLOT_CHECK_NEAR(p.text_backend()->em_px(), 26.0f, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(p.text_backend()->height(), cell * 2.0f, 1e-3);
|
||||||
|
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||||
|
ctx.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
PLOT_TEST(renders_visible_trace_across_viewport) {
|
PLOT_TEST(renders_visible_trace_across_viewport) {
|
||||||
plottest::HeadlessContext ctx(kW, kH);
|
plottest::HeadlessContext ctx(kW, kH);
|
||||||
plot::Status st;
|
plot::Status st;
|
||||||
|
|||||||
@@ -617,9 +617,13 @@ def main(argv=None):
|
|||||||
if args.check:
|
if args.check:
|
||||||
out = Path(args.out)
|
out = Path(args.out)
|
||||||
committed = out.read_bytes() if out.is_file() else None
|
committed = out.read_bytes() if out.is_file() else None
|
||||||
fresh, _ = generate(args.font, args.em, args.supersample)
|
# A missing committed file is drift whatever else is wrong with the
|
||||||
|
# machine, so it is reported before the font is read: otherwise a checkout
|
||||||
|
# with no atlas *and* no installed face would look like an environment
|
||||||
|
# gap (EXIT_BAD_FONT) instead of the failure it is.
|
||||||
if committed is None:
|
if committed is None:
|
||||||
return _fail(EXIT_MISMATCH, f"{out} does not exist; run {SCRIPT} to generate it")
|
return _fail(EXIT_MISMATCH, f"{out} does not exist; run {SCRIPT} to generate it")
|
||||||
|
fresh, _ = generate(args.font, args.em, args.supersample)
|
||||||
if committed != fresh.encode():
|
if committed != fresh.encode():
|
||||||
return _fail(
|
return _fail(
|
||||||
EXIT_MISMATCH,
|
EXIT_MISMATCH,
|
||||||
|
|||||||
Reference in new issue
Block a user