feat(render): anti-aliased embedded Adwaita Mono atlas, generated and committed
This commit is contained in:
1 parent
3ff7b9c978
commit
5ca3587325
17 files changed
+3417
-622
No files matched your search
+1
-1
@@ -16,7 +16,7 @@ add_library(plot_core
|
||||
src/render/shaders.cpp
|
||||
src/render/text.cpp
|
||||
src/render/text_atlas.cpp
|
||||
src/render/bitmap_font.cpp
|
||||
src/render/embedded_mono_font.cpp
|
||||
src/render/primitives.cpp
|
||||
src/render/buffer.cpp
|
||||
src/render/vertex_array.cpp
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
#include <plot/data/series.hpp>
|
||||
#include <plot/data/stream_series.hpp>
|
||||
|
||||
#include <plot/render/bitmap_font.hpp>
|
||||
#include <plot/render/embedded_mono_font.hpp>
|
||||
#include <plot/render/buffer.hpp>
|
||||
#include <plot/render/primitives.hpp>
|
||||
#include <plot/render/program.hpp>
|
||||
|
||||
@@ -23,7 +23,7 @@ enum class TickFormat {
|
||||
/// Writes an SI-prefixed rendering of `v` to `buf`, always NUL-terminated and
|
||||
/// never writing past `n` bytes (a too-small buffer truncates, it does not
|
||||
/// overflow). ASCII plus the UTF-8 micro sign U+00B5 (0xC2 0xB5) for 1e-6, so
|
||||
/// `plot::render::BitmapFont` renders it instead of mojibake.
|
||||
/// `plot::render::EmbeddedMonoFont` renders it instead of mojibake.
|
||||
///
|
||||
/// `format_si(0.0)` is exactly `"0"`. Values use three significant digits, so
|
||||
/// `1234.0` renders as `1.23k`. The sign of a negative value is kept. A value
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
#pragma once
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
#include <plot/render/text.hpp>
|
||||
|
||||
namespace plot::render {
|
||||
|
||||
/// 8x8 bitmap font, the text backend of the renderer.
|
||||
///
|
||||
/// The glyphs are 1-bit, one byte per row, bit 0 the leftmost pixel; the table
|
||||
/// and its provenance are in `src/render/bitmap_font.cpp`. Nothing here needs a
|
||||
/// font file, FreeType or stb_truetype: `plot_core` stays dependency-free.
|
||||
///
|
||||
/// Metrics are proportional, not fixed width: an advance is the glyph's ink
|
||||
/// width plus one pixel of spacing, floored at two pixels, derived from the
|
||||
/// same bitmaps the atlas uploads, so a second width table cannot drift from the
|
||||
/// artwork. Whitespace is a blank glyph with a nonzero advance, and every
|
||||
/// codepoint without a glyph draws a visible box, so a missing glyph is obvious
|
||||
/// rather than silently blank.
|
||||
///
|
||||
/// Metrics are in unscaled font pixels, one per glyph texel, and the atlas is a
|
||||
/// grid of `kGlyphPx` cells: every glyph's rect is its own cell, so a run drawn
|
||||
/// at `s` pixels per font pixel measures `measure(utf8) * s` and sits in a cell
|
||||
/// of `s * kGlyphPx` pixels. `TextBatch::add_text` takes that factor and applies
|
||||
/// it to the pen it advances.
|
||||
class BitmapFont final : public TextBackend {
|
||||
public:
|
||||
// Atlas geometry: a grid of `kGlyphPx` cells whose width is a whole number of
|
||||
// them. A glyph's rect is its own cell, normalised by these sizes, so the grid
|
||||
// is all the text pipeline has to know about the atlas.
|
||||
static constexpr u32 kGlyphPx = 8; ///< glyph cell edge, texels
|
||||
static constexpr u32 kFirstChar = 32; ///< U+0020
|
||||
static constexpr u32 kAsciiGlyphs = 95; ///< U+0020..U+007E
|
||||
static constexpr u32 kDegreeSlot = kAsciiGlyphs; ///< U+00B0 DEGREE SIGN
|
||||
static constexpr u32 kMicroSlot = kAsciiGlyphs + 1u; ///< U+00B5 MICRO SIGN
|
||||
static constexpr u32 kOhmSlot = kAsciiGlyphs + 2u; ///< U+03A9 OHM
|
||||
static constexpr u32 kFallbackSlot = kAsciiGlyphs + 3u; ///< missing-glyph box
|
||||
static constexpr u32 kGlyphCount = kAsciiGlyphs + 4u;
|
||||
static constexpr u32 kAtlasCols = 16;
|
||||
static constexpr u32 kAtlasRows = 7;
|
||||
static constexpr u32 kAtlasWidth = kAtlasCols * kGlyphPx; ///< 128 texels
|
||||
static constexpr u32 kAtlasHeight = kAtlasRows * kGlyphPx; ///< 56 texels
|
||||
|
||||
/// The atlas cell, normalised: one `kGlyphPx` square of the grid. Every glyph's
|
||||
/// rect is this, and `cell_uv()` reports it -- one number, derived from the same
|
||||
/// two constants `pack_atlas()` packs, so the grid the pipeline draws and the
|
||||
/// grid the ink was written into cannot disagree.
|
||||
static constexpr Vec2 kCellUV{static_cast<f32>(kGlyphPx) / static_cast<f32>(kAtlasWidth),
|
||||
static_cast<f32>(kGlyphPx) / static_cast<f32>(kAtlasHeight)};
|
||||
static_assert(kAtlasCols * kAtlasRows >= kGlyphCount, "atlas too small for the glyph set");
|
||||
|
||||
BitmapFont() noexcept = default;
|
||||
|
||||
/// Width of `utf8`, the sum of the glyph advances: zero for the empty string
|
||||
/// and for null. The ASCII path allocates nothing.
|
||||
f32 measure(const char* utf8) const override;
|
||||
|
||||
/// Cell height in pixels (one glyph row of text occupies `height()` pixels).
|
||||
f32 height() const override;
|
||||
/// Top of the cell to the baseline, in pixels.
|
||||
f32 ascent() const override;
|
||||
|
||||
/// Advance of one ASCII character in pixels. A byte that is not printable
|
||||
/// ASCII reports the missing-glyph box, matching what `measure` gives that
|
||||
/// byte on its own.
|
||||
f32 advance(char c) const noexcept;
|
||||
|
||||
/// Advance of a decoded Unicode codepoint, in pixels.
|
||||
f32 advance_codepoint(u32 codepoint) const noexcept;
|
||||
|
||||
/// Glyph slot for a codepoint: printable ASCII is its own slot, the three SI
|
||||
/// codepoints `format_si` emits map to real glyphs, and anything else maps to
|
||||
/// the visible missing-glyph box.
|
||||
static u32 glyph_index(u32 codepoint) noexcept;
|
||||
/// The glyph for `codepoint`: its atlas cell as a normalised rect (see
|
||||
/// `Glyph`), a zero bearing and its ink-derived advance. Never null -- a
|
||||
/// codepoint with no artwork maps to the visible missing-glyph box, which is
|
||||
/// what a label outside the coverage set should show.
|
||||
const Glyph* glyph(u32 codepoint) const override;
|
||||
|
||||
/// `kCellUV`: the 8x8 cell every glyph occupies.
|
||||
Vec2 cell_uv() const override { return kCellUV; }
|
||||
|
||||
/// The packed atlas, as `TextBackend` sees it: the bytes `atlas_image()`
|
||||
/// returns, `kAtlasWidth` by `kAtlasHeight`, GL_R8-ready. Never null.
|
||||
const u8* atlas_pixels() const override;
|
||||
u32 atlas_width() const override;
|
||||
u32 atlas_height() const override;
|
||||
|
||||
/// Always zero: the atlas is a compile-time constant, so it never changes and
|
||||
/// the renderer uploads it once.
|
||||
u32 revision() const override;
|
||||
|
||||
/// Rows in a glyph bitmap, always `kGlyphPx`.
|
||||
u32 glyph_rows(char c) const noexcept;
|
||||
|
||||
/// The `kGlyphPx` rows of one ASCII character's bitmap, top row first.
|
||||
const u8* glyph_bitmap(char c) const noexcept;
|
||||
|
||||
/// The `kGlyphPx` rows of a glyph slot.
|
||||
static const u8* glyph_bitmap_of(u32 slot) noexcept;
|
||||
|
||||
/// The packed atlas image, ready for `plot::gl::Texture::upload_r8`: rows of
|
||||
/// `kAtlasWidth` bytes, top row first, 255 for ink and 0 for background
|
||||
/// (straight alpha, no dilation or gamma applied).
|
||||
static const u8* atlas_image() noexcept;
|
||||
|
||||
/// Texel origin of a glyph slot in the atlas, in texels; `glyph()` gives the
|
||||
/// same rectangle normalised, which is what the text pipeline stages.
|
||||
static Vec2 atlas_origin(u32 slot) noexcept;
|
||||
|
||||
private:
|
||||
/// Every slot's rect, bearing and advance, derived from the bitmaps above at
|
||||
/// compile time. This replaced a per-instance `advances_` array holding the
|
||||
/// same numbers, so the advances the pen uses and the ones `glyph()` publishes
|
||||
/// cannot drift apart.
|
||||
static const Glyph* glyph_table() noexcept;
|
||||
};
|
||||
|
||||
} // namespace plot::render
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
#include <plot/render/text.hpp>
|
||||
|
||||
namespace plot::render {
|
||||
|
||||
/// The renderer's built-in text backend: an anti-aliased greyscale atlas of
|
||||
/// Adwaita Mono, baked at authoring time and committed as data.
|
||||
///
|
||||
/// There is no font file, FreeType or stb_truetype behind this: the coverage
|
||||
/// bytes and the glyph table are generated by `tools/fonts/gen_atlas.py` into
|
||||
/// `src/render/embedded_mono_font.cpp`, so `plot_core` stays dependency-free and
|
||||
/// nothing rasterises at build or run time. That file carries the provenance --
|
||||
/// source face, its version, the em size, the supersample factor and the
|
||||
/// coverage set -- and `gen_atlas.py --check` fails if it has drifted from its
|
||||
/// input. Adwaita Mono is SIL OFL 1.1 (a copy is committed at
|
||||
/// `licences/OFL-1.1.txt`); the class is named neutrally because OFL reserves
|
||||
/// the font name for modified redistributions.
|
||||
///
|
||||
/// **One cell, every glyph.** The atlas is a fixed-cell grid -- `cell_uv()` is
|
||||
/// the contract, and the staging path asserts it per glyph. Each cell is the
|
||||
/// face's maximum advance wide and its ascent-to-descent extent tall, and every
|
||||
/// glyph is drawn into its cell with its bearing preserved, never trimmed to its
|
||||
/// ink. Because the face is monospaced the pen advances by the same amount for
|
||||
/// every glyph, which is the face's own `hmtx` advance and not an assumption.
|
||||
///
|
||||
/// A codepoint outside the coverage set draws the baked fallback box, so a label
|
||||
/// the atlas cannot spell is visible rather than silently blank.
|
||||
class EmbeddedMonoFont final : public TextBackend {
|
||||
public:
|
||||
/// The size the atlas was baked at, in font pixels, and the coverage samples
|
||||
/// per texel. Repeated here so a caller can reason about the artwork without
|
||||
/// opening the generated table; the generated translation unit is the
|
||||
/// authority and pins the same values.
|
||||
static constexpr u32 kEmPx = 13;
|
||||
static constexpr u32 kSupersample = 4;
|
||||
|
||||
/// The codepoint value `codepoint_of_slot()` reports for the fallback slot. It
|
||||
/// is deliberately not a Unicode scalar, so it can never collide with a
|
||||
/// codepoint the atlas really holds.
|
||||
static constexpr u32 kFallbackCodepoint = 0xFFFFFFFFu;
|
||||
|
||||
EmbeddedMonoFont() noexcept = default;
|
||||
|
||||
/// Width of `utf8` in font pixels: the sum of the glyphs' advances, zero for
|
||||
/// the empty string and for null. Allocates nothing.
|
||||
f32 measure(const char* utf8) const override;
|
||||
|
||||
/// Cell height in font pixels: one line of text occupies this many pixels.
|
||||
f32 height() const override;
|
||||
/// Top of the cell to the baseline, in font pixels.
|
||||
f32 ascent() const override;
|
||||
/// `kEmPx`: the size the face was baked at. The renderer scales a run by
|
||||
/// `Style::axis_font_px / em_px()`, so the same backend draws the size the
|
||||
/// style asked for whatever it was baked at.
|
||||
f32 em_px() const override;
|
||||
|
||||
/// The glyph for `codepoint`: its atlas cell as a normalised rect, no extra
|
||||
/// bearing -- the bearing is inside the cell's ink -- and its advance. Never
|
||||
/// null: anything outside the coverage set maps to the fallback box.
|
||||
const Glyph* glyph(u32 codepoint) const override;
|
||||
|
||||
/// `(kCellW / atlas_width(), kCellH / atlas_height())`: the cell every glyph's
|
||||
/// rect is.
|
||||
Vec2 cell_uv() const override;
|
||||
|
||||
/// The packed atlas, GL_R8-ready, rows of `atlas_width()` bytes, top row
|
||||
/// first, one coverage byte per texel. Never null.
|
||||
const u8* atlas_pixels() const override;
|
||||
u32 atlas_width() const override;
|
||||
u32 atlas_height() const override;
|
||||
|
||||
/// Always zero: the atlas is a compile-time constant, so the renderer uploads
|
||||
/// it once and never re-uploads.
|
||||
u32 revision() const override;
|
||||
|
||||
/// Advance of a decoded codepoint, in font pixels. A codepoint outside the
|
||||
/// coverage set reports the fallback box's advance.
|
||||
static f32 advance_codepoint(u32 codepoint) noexcept;
|
||||
|
||||
/// Slot a codepoint maps to: printable ASCII is a direct index, the bakes SI
|
||||
/// and dash codepoints are a short switch, and everything else is the fallback
|
||||
/// box -- so a missing glyph is always visible, never blank.
|
||||
static u32 glyph_index(u32 codepoint) noexcept;
|
||||
|
||||
/// Number of cells in the atlas, the fallback included.
|
||||
static u32 glyph_count() noexcept;
|
||||
|
||||
/// The codepoint a slot was baked for, or `kFallbackCodepoint` for the
|
||||
/// fallback slot. Out-of-range slots report the fallback too.
|
||||
static u32 codepoint_of_slot(u32 slot) noexcept;
|
||||
|
||||
/// The fixed cell size, in atlas texels. Every glyph's rect is exactly this.
|
||||
static u32 cell_width() noexcept;
|
||||
static u32 cell_height() noexcept;
|
||||
|
||||
/// Texel origin of a slot's cell in the atlas; `glyph()` gives the same
|
||||
/// rectangle normalised, which is what the text pipeline stages.
|
||||
static Vec2 atlas_origin(u32 slot) noexcept;
|
||||
|
||||
/// The packed atlas image: the bytes `atlas_pixels()` returns, as a
|
||||
/// compile-time table. Never null.
|
||||
static const u8* atlas_image() noexcept;
|
||||
};
|
||||
|
||||
} // namespace plot::render
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
#include <plot/plot/plot.hpp>
|
||||
#include <plot/render/bitmap_font.hpp>
|
||||
#include <plot/render/embedded_mono_font.hpp>
|
||||
#include <plot/render/buffer.hpp>
|
||||
#include <plot/render/primitives.hpp>
|
||||
#include <plot/render/program.hpp>
|
||||
@@ -155,9 +155,11 @@ class Renderer {
|
||||
bool using_persistent_upload() const noexcept {
|
||||
return line_vao_.persistent() && text_vao_.persistent();
|
||||
}
|
||||
/// The axis text size the renderer actually draws with: `style().axis_font_px`
|
||||
/// rounded to the nearest whole multiple of `render::BitmapFont::kGlyphPx`,
|
||||
/// because a 1-bit glyph only scales cleanly by whole factors.
|
||||
/// The axis text size the renderer actually draws with: the on-screen cell
|
||||
/// height of the backend it draws with. `Style::axis_font_px` is an em size, so
|
||||
/// with an ascent-to-descent cell it is this that a host reads back as the line
|
||||
/// height it got -- and it is the backend's own `height()` scaled, so it always
|
||||
/// agrees with the metrics the plot laid the labels out with.
|
||||
f32 axis_font_px() const noexcept { return font_backend_.cell_px(); }
|
||||
|
||||
/// Writes the framebuffer's viewport region as a binary PPM (P6, 8 bits per
|
||||
@@ -174,23 +176,25 @@ class Renderer {
|
||||
bool dump_frame_ppm(const char* path, Status* st = nullptr) const;
|
||||
|
||||
private:
|
||||
/// The bitmap font as the plot layer sees it, at the size the text pipeline
|
||||
/// draws it: `measure`/`height`/`ascent` scale with the glyph cell, so a label
|
||||
/// can never be laid out for one size and drawn at another. The 8x8 font scales
|
||||
/// by whole multiples of its cell only -- anything else lands a 1-bit glyph's
|
||||
/// strokes unevenly -- so `reset()` picks the multiple nearest the requested
|
||||
/// size.
|
||||
/// The backend the plot layer sees, at the size the text pipeline draws it:
|
||||
/// `measure`/`height`/`ascent` scale with the run, so a label can never be laid
|
||||
/// out for one size and drawn at another.
|
||||
///
|
||||
/// It is also the backend the renderer uploads the atlas from, so it forwards
|
||||
/// the whole interface: the metrics and the glyph bearings and advances are
|
||||
/// scaled, while the artwork itself -- the atlas bytes, its size and its
|
||||
/// revision -- is the font's own and passes through untouched.
|
||||
/// It wraps a `TextBackend` and nothing more: the scale is
|
||||
/// `axis_font_px / em_px()`, a plain ratio, so an anti-aliased face is drawn at
|
||||
/// the size the style asked for rather than at a whole multiple of its cell,
|
||||
/// and any backend -- the embedded font, a host's TrueType face -- is drawn the
|
||||
/// same way. It forwards the whole interface: the metrics and the glyph
|
||||
/// bearings and advances are scaled, while the artwork itself -- the atlas
|
||||
/// bytes, its size and its revision -- is the backend's own and passes through
|
||||
/// untouched.
|
||||
class AxisFont final : public render::TextBackend {
|
||||
public:
|
||||
void reset(const render::BitmapFont& font, f32 requested_px) noexcept;
|
||||
void reset(const render::TextBackend& font, f32 requested_px) noexcept;
|
||||
f32 measure(const char* utf8) const override;
|
||||
f32 height() const override;
|
||||
f32 ascent() const override;
|
||||
f32 em_px() const override;
|
||||
/// The font's glyph at this wrapper's scale: the atlas rect is unchanged (it
|
||||
/// is scale-free) and the bearing and advance are multiplied by it, so one
|
||||
/// `Glyph` describes the glyph this backend draws. The result lives in the
|
||||
@@ -213,7 +217,7 @@ class Renderer {
|
||||
f32 scale() const noexcept { return scale_; }
|
||||
|
||||
private:
|
||||
const render::BitmapFont* font_ = nullptr;
|
||||
const render::TextBackend* font_ = nullptr;
|
||||
f32 cell_px_ = 1.0f;
|
||||
f32 scale_ = 1.0f;
|
||||
/// `glyph()`'s scaled result, returned by pointer because the interface is
|
||||
@@ -280,7 +284,10 @@ class Renderer {
|
||||
void draw_measures(const Plot& plot, const PlotLayout& layout);
|
||||
void draw_annotations(const Plot& plot);
|
||||
|
||||
render::BitmapFont font_;
|
||||
/// The built-in backend the renderer owns. Every use of it goes through the
|
||||
/// `TextBackend` interface -- `self`-typed nowhere -- so swapping it for
|
||||
/// another face is a change to this member alone.
|
||||
render::EmbeddedMonoFont default_font_;
|
||||
AxisFont font_backend_;
|
||||
render::Style style_;
|
||||
|
||||
|
||||
@@ -108,6 +108,18 @@ class TextBackend {
|
||||
/// 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
|
||||
/// it holds. A backend with no meaningful em (one whose "font pixels" are an
|
||||
/// arbitrary unit) returns something proportional, `height()` being the
|
||||
/// natural choice.
|
||||
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
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
Copyright (c) 2016, The Inter Project Authors (https://github.com/rsms/inter)
|
||||
Copyright (c) 2015-2025, Renzhi Li (aka. Belleve Invis, belleve@typeof.net)
|
||||
Copyright (c) 2025, Jamie Gravendeel (https://jamie.garden)
|
||||
Copyright (c) 2025, Florian Müllner
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,264 +0,0 @@
|
||||
#include <plot/render/bitmap_font.hpp>
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace plot::render {
|
||||
namespace {
|
||||
|
||||
// Glyph data and its provenance
|
||||
// -----------------------------
|
||||
// The 95 printable ASCII glyphs (slots 0..94, U+0020..U+007E) are the
|
||||
// `font8x8_basic` table of the font8x8 project by Daniel Hepper, released into
|
||||
// the public domain. It is itself based on Marcel Sondaar's public-domain 8x8
|
||||
// VGA font (via the IBM public-domain VGA fonts). Fetched on 2026-09-27 from
|
||||
// https://raw.githubusercontent.com/dhepper/font8x8/master/font8x8_basic.h
|
||||
// The glyphs the plot layer's SI labels need come from the same project:
|
||||
// U+00B0 DEGREE SIGN and U+00B5 MICRO SIGN from font8x8_ext_latin.h (which
|
||||
// covers U+00A0..U+00FF), and U+03A9 GREEK CAPITAL LETTER OMEGA from
|
||||
// font8x8_greek.h (which covers U+0390..U+03C9).
|
||||
// The missing-glyph box in the last slot is authored here: a hollow 8x8 square.
|
||||
// Nothing else in this table is hand-drawn, and none of it is a library: it is
|
||||
// public-domain data, so `plot_core` still links nothing third-party.
|
||||
//
|
||||
// Row 0 is the top row and bit 0 is the leftmost pixel, one byte per row.
|
||||
|
||||
constexpr u8 kGlyphBits[BitmapFont::kGlyphCount][BitmapFont::kGlyphPx] = {
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 0] U+0020 space
|
||||
{0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // [ 1] U+0021 '!'
|
||||
{0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 2] U+0022 '"'
|
||||
{0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // [ 3] U+0023 '#'
|
||||
{0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // [ 4] U+0024 '$'
|
||||
{0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // [ 5] U+0025 '%'
|
||||
{0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // [ 6] U+0026 '&'
|
||||
{0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // [ 7] U+0027 '''
|
||||
{0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // [ 8] U+0028 '('
|
||||
{0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // [ 9] U+0029 ')'
|
||||
{0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // [10] U+002A '*'
|
||||
{0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // [11] U+002B '+'
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // [12] U+002C ','
|
||||
{0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // [13] U+002D '-'
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // [14] U+002E '.'
|
||||
{0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // [15] U+002F '/'
|
||||
{0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // [16] U+0030 '0'
|
||||
{0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // [17] U+0031 '1'
|
||||
{0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // [18] U+0032 '2'
|
||||
{0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // [19] U+0033 '3'
|
||||
{0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // [20] U+0034 '4'
|
||||
{0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // [21] U+0035 '5'
|
||||
{0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // [22] U+0036 '6'
|
||||
{0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // [23] U+0037 '7'
|
||||
{0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // [24] U+0038 '8'
|
||||
{0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // [25] U+0039 '9'
|
||||
{0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // [26] U+003A ':'
|
||||
{0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // [27] U+003B ';'
|
||||
{0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // [28] U+003C '<'
|
||||
{0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // [29] U+003D '='
|
||||
{0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // [30] U+003E '>'
|
||||
{0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // [31] U+003F '?'
|
||||
{0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // [32] U+0040 '@'
|
||||
{0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // [33] U+0041 'A'
|
||||
{0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // [34] U+0042 'B'
|
||||
{0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // [35] U+0043 'C'
|
||||
{0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // [36] U+0044 'D'
|
||||
{0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // [37] U+0045 'E'
|
||||
{0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // [38] U+0046 'F'
|
||||
{0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // [39] U+0047 'G'
|
||||
{0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // [40] U+0048 'H'
|
||||
{0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [41] U+0049 'I'
|
||||
{0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // [42] U+004A 'J'
|
||||
{0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // [43] U+004B 'K'
|
||||
{0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // [44] U+004C 'L'
|
||||
{0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // [45] U+004D 'M'
|
||||
{0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // [46] U+004E 'N'
|
||||
{0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // [47] U+004F 'O'
|
||||
{0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // [48] U+0050 'P'
|
||||
{0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // [49] U+0051 'Q'
|
||||
{0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // [50] U+0052 'R'
|
||||
{0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // [51] U+0053 'S'
|
||||
{0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [52] U+0054 'T'
|
||||
{0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // [53] U+0055 'U'
|
||||
{0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // [54] U+0056 'V'
|
||||
{0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // [55] U+0057 'W'
|
||||
{0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // [56] U+0058 'X'
|
||||
{0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // [57] U+0059 'Y'
|
||||
{0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // [58] U+005A 'Z'
|
||||
{0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // [59] U+005B '['
|
||||
{0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // [60] U+005C '\'
|
||||
{0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // [61] U+005D ']'
|
||||
{0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // [62] U+005E '^'
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // [63] U+005F '_'
|
||||
{0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // [64] U+0060 '`'
|
||||
{0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // [65] U+0061 'a'
|
||||
{0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // [66] U+0062 'b'
|
||||
{0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // [67] U+0063 'c'
|
||||
{0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00}, // [68] U+0064 'd'
|
||||
{0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00}, // [69] U+0065 'e'
|
||||
{0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00}, // [70] U+0066 'f'
|
||||
{0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // [71] U+0067 'g'
|
||||
{0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // [72] U+0068 'h'
|
||||
{0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [73] U+0069 'i'
|
||||
{0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // [74] U+006A 'j'
|
||||
{0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // [75] U+006B 'k'
|
||||
{0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // [76] U+006C 'l'
|
||||
{0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // [77] U+006D 'm'
|
||||
{0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // [78] U+006E 'n'
|
||||
{0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // [79] U+006F 'o'
|
||||
{0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // [80] U+0070 'p'
|
||||
{0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // [81] U+0071 'q'
|
||||
{0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // [82] U+0072 'r'
|
||||
{0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // [83] U+0073 's'
|
||||
{0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // [84] U+0074 't'
|
||||
{0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // [85] U+0075 'u'
|
||||
{0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // [86] U+0076 'v'
|
||||
{0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // [87] U+0077 'w'
|
||||
{0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // [88] U+0078 'x'
|
||||
{0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // [89] U+0079 'y'
|
||||
{0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // [90] U+007A 'z'
|
||||
{0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // [91] U+007B '{'
|
||||
{0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // [92] U+007C '|'
|
||||
{0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // [93] U+007D '}'
|
||||
{0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // [94] U+007E '~'
|
||||
{0x1C, 0x36, 0x36, 0x1C, 0x00, 0x00, 0x00, 0x00}, // [95] U+00B0 degree
|
||||
{0x00, 0x00, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x03}, // [96] U+00B5 micro
|
||||
{0x3E, 0x63, 0x63, 0x63, 0x36, 0x36, 0x77, 0x00}, // [97] U+03A9 ohm
|
||||
{0x7E, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00}, // [98] missing-glyph box, authored here
|
||||
};
|
||||
|
||||
/// Repacks the glyph-major table into the atlas image the text pipeline
|
||||
/// uploads: one 8x8 cell per slot, 255 for ink, straight alpha, no dilation.
|
||||
constexpr std::array<u8, BitmapFont::kAtlasWidth * BitmapFont::kAtlasHeight> pack_atlas() noexcept {
|
||||
std::array<u8, BitmapFont::kAtlasWidth * BitmapFont::kAtlasHeight> image{};
|
||||
for (u32 slot = 0; slot < BitmapFont::kGlyphCount; ++slot) {
|
||||
const u32 cell_x = (slot % BitmapFont::kAtlasCols) * BitmapFont::kGlyphPx;
|
||||
const u32 cell_y = (slot / BitmapFont::kAtlasCols) * BitmapFont::kGlyphPx;
|
||||
for (u32 y = 0; y < BitmapFont::kGlyphPx; ++y) {
|
||||
const u8 bits = kGlyphBits[slot][y];
|
||||
for (u32 x = 0; x < BitmapFont::kGlyphPx; ++x) {
|
||||
image[(cell_y + y) * BitmapFont::kAtlasWidth + cell_x + x] =
|
||||
static_cast<u8>(((bits >> x) & 1u) != 0u ? 0xFFu : 0x00u);
|
||||
}
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
constexpr std::array<u8, BitmapFont::kAtlasWidth * BitmapFont::kAtlasHeight> kAtlasImage =
|
||||
pack_atlas();
|
||||
|
||||
/// Span of set bits in a glyph's rows: 0 for a blank glyph such as space.
|
||||
constexpr u32 ink_width(const u8* rows) noexcept {
|
||||
u32 lo = BitmapFont::kGlyphPx;
|
||||
u32 hi = 0;
|
||||
bool any = false;
|
||||
for (u32 y = 0; y < BitmapFont::kGlyphPx; ++y) {
|
||||
for (u32 x = 0; x < BitmapFont::kGlyphPx; ++x) {
|
||||
if (((rows[y] >> x) & 1u) != 0u) {
|
||||
if (x < lo) {
|
||||
lo = x;
|
||||
}
|
||||
if (x > hi) {
|
||||
hi = x;
|
||||
}
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return any ? hi - lo + 1u : 0u;
|
||||
}
|
||||
|
||||
/// The glyph table the pipeline stages from: one entry per slot, its rect at the
|
||||
/// slot's place in the grid and one cell big -- the cell `cell_uv()` declares,
|
||||
/// so a glyph's rect and the size the draw is told cannot disagree -- a zero
|
||||
/// bearing (the ink sits at the cell's top-left) and the ink-derived advance.
|
||||
/// Built from the same tables `pack_atlas()` packs.
|
||||
constexpr std::array<Glyph, BitmapFont::kGlyphCount> build_glyphs() noexcept {
|
||||
std::array<Glyph, BitmapFont::kGlyphCount> glyphs{};
|
||||
for (u32 slot = 0; slot < BitmapFont::kGlyphCount; ++slot) {
|
||||
const f32 u0 = static_cast<f32>(slot % BitmapFont::kAtlasCols) * BitmapFont::kCellUV.x;
|
||||
const f32 v0 = static_cast<f32>(slot / BitmapFont::kAtlasCols) * BitmapFont::kCellUV.y;
|
||||
const u32 ink = ink_width(kGlyphBits[slot]);
|
||||
const u32 spaced = ink + 1u; // one pixel of side bearing, floored at two
|
||||
glyphs[slot] = Glyph{u0, v0, u0 + BitmapFont::kCellUV.x, v0 + BitmapFont::kCellUV.y, 0.0f,
|
||||
0.0f, static_cast<f32>(spaced < 2u ? 2u : spaced)};
|
||||
}
|
||||
return glyphs;
|
||||
}
|
||||
|
||||
constexpr std::array<Glyph, BitmapFont::kGlyphCount> kGlyphs = build_glyphs();
|
||||
|
||||
} // namespace
|
||||
|
||||
const Glyph* BitmapFont::glyph_table() noexcept { return kGlyphs.data(); }
|
||||
|
||||
f32 BitmapFont::measure(const char* utf8) const {
|
||||
if (utf8 == nullptr) {
|
||||
return 0.0f;
|
||||
}
|
||||
f32 width = 0.0f;
|
||||
for (const char* p = utf8; *p != '\0';) {
|
||||
width += advance_codepoint(next_codepoint(p));
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
const Glyph* BitmapFont::glyph(u32 codepoint) const {
|
||||
// Every codepoint has a glyph: `glyph_index` maps whatever the font cannot
|
||||
// draw -- including `kInvalidCodepoint` -- to the visible box.
|
||||
return &glyph_table()[glyph_index(codepoint)];
|
||||
}
|
||||
|
||||
const u8* BitmapFont::atlas_pixels() const { return atlas_image(); }
|
||||
|
||||
u32 BitmapFont::atlas_width() const { return kAtlasWidth; }
|
||||
|
||||
u32 BitmapFont::atlas_height() const { return kAtlasHeight; }
|
||||
|
||||
u32 BitmapFont::revision() const { return 0u; }
|
||||
|
||||
f32 BitmapFont::height() const { return static_cast<f32>(kGlyphPx); }
|
||||
|
||||
f32 BitmapFont::ascent() const { return static_cast<f32>(kGlyphPx - 1u); }
|
||||
|
||||
f32 BitmapFont::advance(char c) const noexcept {
|
||||
return glyph_table()[glyph_index(static_cast<u32>(static_cast<unsigned char>(c)))].advance;
|
||||
}
|
||||
|
||||
f32 BitmapFont::advance_codepoint(u32 codepoint) const noexcept {
|
||||
return glyph_table()[glyph_index(codepoint)].advance;
|
||||
}
|
||||
|
||||
u32 BitmapFont::glyph_index(u32 codepoint) noexcept {
|
||||
if (codepoint >= kFirstChar && codepoint < kFirstChar + kAsciiGlyphs) {
|
||||
return codepoint - kFirstChar;
|
||||
}
|
||||
switch (codepoint) {
|
||||
case 0x00B0u:
|
||||
return kDegreeSlot;
|
||||
case 0x00B5u:
|
||||
return kMicroSlot;
|
||||
case 0x03A9u:
|
||||
return kOhmSlot;
|
||||
default:
|
||||
return kFallbackSlot;
|
||||
}
|
||||
}
|
||||
|
||||
u32 BitmapFont::glyph_rows(char) const noexcept { return kGlyphPx; }
|
||||
|
||||
const u8* BitmapFont::glyph_bitmap(char c) const noexcept {
|
||||
return glyph_bitmap_of(glyph_index(static_cast<u32>(static_cast<unsigned char>(c))));
|
||||
}
|
||||
|
||||
const u8* BitmapFont::glyph_bitmap_of(u32 slot) noexcept {
|
||||
return kGlyphBits[slot < kGlyphCount ? slot : kFallbackSlot];
|
||||
}
|
||||
|
||||
const u8* BitmapFont::atlas_image() noexcept { return kAtlasImage.data(); }
|
||||
|
||||
Vec2 BitmapFont::atlas_origin(u32 slot) noexcept {
|
||||
const u32 s = slot < kGlyphCount ? slot : kFallbackSlot;
|
||||
return Vec2{static_cast<f32>((s % kAtlasCols) * kGlyphPx),
|
||||
static_cast<f32>((s / kAtlasCols) * kGlyphPx)};
|
||||
}
|
||||
|
||||
} // namespace plot::render
|
||||
File diff suppressed because it is too large.
Load diff
+24
-16
@@ -88,23 +88,25 @@ f32 walk_text_clipped(Vec2 pen, const char* utf8, f32 budget_px, const render::T
|
||||
void warn_replaced_text_backend(const render::TextBackend* previous) noexcept {
|
||||
std::fprintf(stderr,
|
||||
"plot::Renderer::draw: replacing the Plot's TextBackend (%p) with the renderer's "
|
||||
"own bitmap font; a plot drawn by a Renderer measures the labels it draws.\n",
|
||||
"own embedded font; a plot drawn by a Renderer measures the labels it draws.\n",
|
||||
static_cast<const void*>(previous));
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void Renderer::AxisFont::reset(const render::BitmapFont& font, f32 requested_px) noexcept {
|
||||
void Renderer::AxisFont::reset(const render::TextBackend& font, f32 requested_px) noexcept {
|
||||
font_ = &font;
|
||||
const f32 native = static_cast<f32>(render::BitmapFont::kGlyphPx);
|
||||
// Whole multiples of the native cell only: a 1-bit glyph drawn at 1.5x lands
|
||||
// its strokes on uneven pixel counts, and the layout has to agree with what the
|
||||
// pipeline draws, so the requested size picks the nearest multiple.
|
||||
f32 steps = std::round(requested_px / native);
|
||||
if (!(steps >= 1.0f)) steps = 1.0f; // also catches NaN
|
||||
cell_px_ = steps * native;
|
||||
scale_ = cell_px_ / native;
|
||||
// The requested size is an em size, exactly as `em_px()` reports the size the
|
||||
// atlas was baked at, so the ratio between them is all the scaling there is:
|
||||
// an anti-aliased face is legible at any ratio, and a style asking for 12 px
|
||||
// from a 13 px bake gets 12/13 rather than a snapped whole multiple. A backend
|
||||
// that cannot say (`em_px()` at or below zero, or NaN) keeps a 1:1 scale rather
|
||||
// than dividing by it.
|
||||
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_;
|
||||
}
|
||||
|
||||
f32 Renderer::AxisFont::measure(const char* utf8) const {
|
||||
@@ -119,6 +121,12 @@ f32 Renderer::AxisFont::ascent() const {
|
||||
return font_ != nullptr ? font_->ascent() * scale_ : 0.0f;
|
||||
}
|
||||
|
||||
f32 Renderer::AxisFont::em_px() const {
|
||||
// The em is the one metric that does not scale: it is the face's own unit, and
|
||||
// it is what the scale above was computed from.
|
||||
return font_ != nullptr ? font_->em_px() : 0.0f;
|
||||
}
|
||||
|
||||
const render::Glyph* Renderer::AxisFont::glyph(u32 codepoint) const {
|
||||
if (font_ == nullptr) {
|
||||
return nullptr; // nothing to draw with, so nothing to describe
|
||||
@@ -169,7 +177,7 @@ bool Renderer::init(Status* st) noexcept {
|
||||
const render::UploadMode mode =
|
||||
persistent ? render::UploadMode::PersistentMapped : render::UploadMode::Orphan;
|
||||
|
||||
font_backend_.reset(font_, style_.axis_font_px);
|
||||
font_backend_.reset(default_font_, style_.axis_font_px);
|
||||
|
||||
// The atlas is the backend's: `sync()` uploads whatever it exposes, at whatever
|
||||
// size it says, and restores the host's unpack alignment around the upload
|
||||
@@ -263,7 +271,7 @@ void Renderer::draw(Plot& plot) {
|
||||
// The plot reserves its label strips from TextBackend metrics, so it must
|
||||
// measure with the size the text pipeline draws with, and re-read the style
|
||||
// each frame so a changed axis_font_px takes effect on the next one.
|
||||
font_backend_.reset(font_, style_.axis_font_px);
|
||||
font_backend_.reset(default_font_, style_.axis_font_px);
|
||||
// The cell every glyph staged this frame is drawn in, declared from the backend
|
||||
// rather than left to whichever glyph is staged first: the instances carry rect
|
||||
// origins, and the draw carries this size (see TextBackend::cell_uv).
|
||||
@@ -280,7 +288,7 @@ void Renderer::draw(Plot& plot) {
|
||||
}
|
||||
#ifndef NDEBUG
|
||||
// The renderer owns the text of the frames it draws: every label goes through
|
||||
// the text pipeline in `font_`, so the plot is handed this font's metrics
|
||||
// the text pipeline in `default_font_`, so the plot is handed this backend's metrics
|
||||
// whatever a host installed. A host backend is therefore replaced on the first
|
||||
// draw -- said out loud in a debug build, since its disappearance is otherwise
|
||||
// silent -- and a Plot nobody draws keeps it (see Plot::set_text_backend()).
|
||||
@@ -456,17 +464,17 @@ void Renderer::stage_xy(const f32* xy, u32 n, bool log_x, bool log_y) {
|
||||
|
||||
Vec2 Renderer::stage_text(Vec2 pos_px, const char* utf8) {
|
||||
// Text is staged in viewport pixels: the text program has no offset uniform.
|
||||
return glyphs_.add_text(utf8, to_viewport(pos_px), font_backend_.scale(), font_);
|
||||
return glyphs_.add_text(utf8, to_viewport(pos_px), font_backend_.scale(), default_font_);
|
||||
}
|
||||
|
||||
f32 Renderer::clipped_text_width(const char* utf8, f32 budget_px) const {
|
||||
return walk_text_clipped(Vec2{}, utf8, budget_px, font_, font_backend_.scale(),
|
||||
return walk_text_clipped(Vec2{}, utf8, budget_px, default_font_, font_backend_.scale(),
|
||||
[](Vec2, const render::Glyph&) {});
|
||||
}
|
||||
|
||||
f32 Renderer::stage_text_clipped(Vec2 pos_px, const char* utf8, f32 budget_px) {
|
||||
const f32 scale = font_backend_.scale();
|
||||
return walk_text_clipped(to_viewport(pos_px), utf8, budget_px, font_, scale,
|
||||
return walk_text_clipped(to_viewport(pos_px), utf8, budget_px, default_font_, scale,
|
||||
[this, scale](Vec2 pen, const render::Glyph& glyph) {
|
||||
glyphs_.add_glyph(pen, glyph, scale);
|
||||
});
|
||||
|
||||
@@ -70,7 +70,12 @@ target_include_directories(plot_tests PRIVATE "${CMAKE_SOURCE_DIR}/adapters")
|
||||
target_compile_definitions(plot_tests PRIVATE PLOT_PYRAMID_COUNT_COVERS
|
||||
PLOT_PYRAMID_COUNT_MERGES
|
||||
PLOT_RING_STATS
|
||||
PLOT_COUNT_MEASURE_RECOMPUTES)
|
||||
PLOT_COUNT_MEASURE_RECOMPUTES
|
||||
# The committed source root, so the
|
||||
# atlas-generator case can run
|
||||
# tools/fonts/gen_atlas.py --check
|
||||
# against the committed atlas.
|
||||
PLOT_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
|
||||
plot_apply_warnings(plot_tests)
|
||||
|
||||
add_test(NAME plot_tests COMMAND plot_tests)
|
||||
|
||||
+36
-20
@@ -6,13 +6,14 @@
|
||||
#include <plot/gl/caps.hpp>
|
||||
#include <plot/gl/handle.hpp>
|
||||
#include <plot/gl/loader.hpp>
|
||||
#include <plot/render/bitmap_font.hpp>
|
||||
#include <plot/render/embedded_mono_font.hpp>
|
||||
#include <plot/render/primitives.hpp>
|
||||
#include <plot/render/program.hpp>
|
||||
#include <plot/render/shaders.hpp>
|
||||
#include <plot/render/text_atlas.hpp>
|
||||
#include <plot/render/vertex_array.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -341,10 +342,10 @@ PLOT_TEST(line_pipeline_draws_an_aa_strip) {
|
||||
|
||||
// The text pipeline end to end: the R8 atlas upload, the per-instance glyph UV
|
||||
// rect, the NEAREST texel-centre sampling, the tint and the same single y flip.
|
||||
// Every one of the cell's 64 pixels is checked against the glyph bitmap, so a
|
||||
// wrong atlas cell, a wrong UV or a transposed axis cannot pass.
|
||||
// Every texel of the cell is checked against the backend's own coverage byte, so
|
||||
// a wrong atlas cell, a wrong UV or a transposed axis cannot pass.
|
||||
PLOT_TEST(text_pipeline_draws_a_glyph) {
|
||||
using plot::render::BitmapFont;
|
||||
using plot::render::EmbeddedMonoFont;
|
||||
plottest::HeadlessContext ctx(256, 256);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
@@ -352,12 +353,12 @@ PLOT_TEST(text_pipeline_draws_a_glyph) {
|
||||
|
||||
// The backend's atlas, through the owner the renderer uses: single channel,
|
||||
// GL_NEAREST, straight alpha, uploaded from the backend's own bytes.
|
||||
BitmapFont font;
|
||||
EmbeddedMonoFont font;
|
||||
plot::render::TextAtlas atlas;
|
||||
PLOT_CHECK(atlas.sync(font, &st));
|
||||
PLOT_CHECK(!atlas.empty());
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(BitmapFont::kAtlasWidth), 1e-6);
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(BitmapFont::kAtlasHeight), 1e-6);
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(font.atlas_width()), 1e-6);
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(font.atlas_height()), 1e-6);
|
||||
|
||||
plot::render::TextProgram program;
|
||||
PLOT_CHECK(program.create(&st));
|
||||
@@ -383,29 +384,44 @@ PLOT_TEST(text_pipeline_draws_a_glyph) {
|
||||
|
||||
program.bind();
|
||||
program.set_viewport({static_cast<f32>(ctx.width), static_cast<f32>(ctx.height)});
|
||||
program.set_px_range({static_cast<f32>(BitmapFont::kGlyphPx), static_cast<f32>(BitmapFont::kGlyphPx)});
|
||||
// The backend's own cell, on screen at 1:1: the draw's one rect size.
|
||||
const plot::f32 cell_w = static_cast<plot::f32>(EmbeddedMonoFont::cell_width());
|
||||
const plot::f32 cell_h = static_cast<plot::f32>(EmbeddedMonoFont::cell_height());
|
||||
program.set_px_range({cell_w, cell_h});
|
||||
program.set_glyph_uv(font.cell_uv());
|
||||
program.set_texture_size(atlas.texel_size());
|
||||
program.set_color(plot::Color::hex(0xFF00FF));
|
||||
// White, so the blended pixel value *is* the coverage byte: the shader emits
|
||||
// `vec4(rgb, a * coverage)`, and a white tint over the black clear lands at
|
||||
// `1.0 * coverage` in every channel.
|
||||
program.set_color(plot::Color::hex(0xFFFFFF));
|
||||
atlas.bind();
|
||||
glyphs.bind();
|
||||
plot::gl::glDrawArraysInstanced(plot::gl::GL_TRIANGLE_STRIP, 0, 4, 1);
|
||||
|
||||
// One cell is 8x8 texels and u_px_range is 8, so texel (gx, gy) lands on one
|
||||
// target pixel: Rect x = 64 + gx, and Rect y = 100 + gy is GL row 155 - gy.
|
||||
const plot::u8* rows = font.glyph_bitmap('A');
|
||||
// The cell is cell_w x cell_h texels and u_px_range is the same, so texel
|
||||
// (gx, gy) lands on exactly one target pixel: Rect x = 64 + gx, and Rect y =
|
||||
// 100 + gy is GL row 155 - gy. A transposed axis, a wrong cell or a wrong UV
|
||||
// all move ink away from where the coverage byte says it is.
|
||||
const plot::u32 slot = EmbeddedMonoFont::glyph_index(static_cast<plot::u32>('A'));
|
||||
const plot::Vec2 origin = EmbeddedMonoFont::atlas_origin(slot);
|
||||
const plot::u8* atlas_bytes = font.atlas_pixels();
|
||||
plot::u32 antialiased = 0;
|
||||
plot::u8 px[4] = {};
|
||||
for (plot::u32 gy = 0; gy < BitmapFont::kGlyphPx; ++gy) {
|
||||
for (plot::u32 gx = 0; gx < BitmapFont::kGlyphPx; ++gx) {
|
||||
const bool ink = ((rows[gy] >> gx) & 1u) != 0u;
|
||||
for (plot::u32 gy = 0; gy < EmbeddedMonoFont::cell_height(); ++gy) {
|
||||
for (plot::u32 gx = 0; gx < EmbeddedMonoFont::cell_width(); ++gx) {
|
||||
const plot::u32 ax = static_cast<plot::u32>(origin.x) + gx;
|
||||
const plot::u32 ay = static_cast<plot::u32>(origin.y) + gy;
|
||||
const plot::u8 want = atlas_bytes[static_cast<std::size_t>(ay) * font.atlas_width() + ax];
|
||||
PLOT_CHECK(ctx.read_pixel(64u + gx, 155u - gy, px));
|
||||
if (ink) {
|
||||
PLOT_CHECK(px[0] > 200 && px[1] < 60 && px[2] > 200);
|
||||
} else {
|
||||
PLOT_CHECK(px[0] < 40 && px[2] < 40);
|
||||
}
|
||||
PLOT_CHECK_NEAR(px[0], static_cast<plot::f32>(want), 2.0);
|
||||
PLOT_CHECK_NEAR(px[1], static_cast<plot::f32>(want), 2.0);
|
||||
PLOT_CHECK_NEAR(px[2], static_cast<plot::f32>(want), 2.0);
|
||||
if (want > 0u && want < 255u) ++antialiased;
|
||||
}
|
||||
}
|
||||
// 'A' has grey edge texels and they reached the framebuffer: a 1-bit atlas
|
||||
// could only ever have produced 0 or 255 here.
|
||||
PLOT_CHECK(antialiased > 0u);
|
||||
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||
ctx.destroy();
|
||||
}
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
#include <plot/plot/detail/measure_count.hpp>
|
||||
#include <plot/data/array_series.hpp>
|
||||
#include <plot/data/stream_series.hpp>
|
||||
#include <plot/render/bitmap_font.hpp>
|
||||
#include <plot/render/embedded_mono_font.hpp>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
@@ -73,7 +73,7 @@ PLOT_TEST(layout_without_text_backend_uses_default_label_areas) {
|
||||
}
|
||||
|
||||
PLOT_TEST(layout_derives_label_areas_from_font_metrics) {
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::Plot p;
|
||||
p.set_text_backend(&font);
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
@@ -103,7 +103,7 @@ PLOT_TEST(layout_derives_label_areas_from_font_metrics) {
|
||||
/// Every titled axis of a direction gets its own text line inside its strip;
|
||||
/// without that, the second titled axis of an edge is drawn on top of the first.
|
||||
PLOT_TEST(layout_stacks_one_title_line_per_titled_axis) {
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::Plot p;
|
||||
p.set_text_backend(&font);
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
@@ -127,7 +127,7 @@ PLOT_TEST(layout_stacks_one_title_line_per_titled_axis) {
|
||||
/// An axis with no title reserves nothing: the strips exist so that a titled axis
|
||||
/// has somewhere outside `axes` to draw in, not as a fixed cost per plot.
|
||||
PLOT_TEST(layout_reserves_no_title_strip_without_a_title) {
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::Plot p;
|
||||
p.set_text_backend(&font);
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
|
||||
+280
-129
@@ -1,12 +1,17 @@
|
||||
// CPU-side contract of the render layer: the staging batches that feed the
|
||||
// instanced line and text pipelines, and the bitmap font's metrics and atlas.
|
||||
// instanced line and text pipelines, and the embedded font's metrics and atlas.
|
||||
// Nothing here touches GL, so it runs on any machine that can build the tests.
|
||||
#include "harness.hpp"
|
||||
|
||||
#include <plot/render/bitmap_font.hpp>
|
||||
#include <plot/render/embedded_mono_font.hpp>
|
||||
#include <plot/render/primitives.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <sys/wait.h>
|
||||
#include <vector>
|
||||
|
||||
PLOT_TEST(line_batch_emits_two_verts_per_segment) {
|
||||
plot::render::LineBatch b;
|
||||
@@ -99,152 +104,293 @@ PLOT_TEST(line_batch_reuses_capacity_across_frames) {
|
||||
PLOT_CHECK(b.data().data() == store);
|
||||
}
|
||||
|
||||
PLOT_TEST(text_batch_measures_ascii) {
|
||||
plot::render::BitmapFont f;
|
||||
const f32 w = f.measure("12.5 ms");
|
||||
PLOT_CHECK(w > 0.0f);
|
||||
namespace {
|
||||
|
||||
using plot::render::EmbeddedMonoFont;
|
||||
|
||||
/// The coverage byte of one texel of `slot`'s cell: 0 background, 255 full ink.
|
||||
plot::u8 cell_texel(const EmbeddedMonoFont& font, plot::u32 slot, plot::u32 col, plot::u32 row) {
|
||||
const plot::Vec2 origin = EmbeddedMonoFont::atlas_origin(slot);
|
||||
const plot::u32 x = static_cast<plot::u32>(origin.x) + col;
|
||||
const plot::u32 y = static_cast<plot::u32>(origin.y) + row;
|
||||
const std::size_t index = static_cast<std::size_t>(y) * font.atlas_width() + x;
|
||||
return font.atlas_image()[index];
|
||||
}
|
||||
|
||||
/// Total coverage of one cell: zero means the cell rasterised blank.
|
||||
plot::u32 cell_ink_units(const EmbeddedMonoFont& font, plot::u32 slot) {
|
||||
plot::u32 total = 0u;
|
||||
for (plot::u32 y = 0; y < EmbeddedMonoFont::cell_height(); ++y) {
|
||||
for (plot::u32 x = 0; x < EmbeddedMonoFont::cell_width(); ++x) {
|
||||
total += cell_texel(font, slot, x, y);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// The fallback slot, found by the marker codepoint rather than assumed.
|
||||
plot::u32 fallback_slot() {
|
||||
for (plot::u32 slot = 0; slot < EmbeddedMonoFont::glyph_count(); ++slot) {
|
||||
if (EmbeddedMonoFont::codepoint_of_slot(slot) == EmbeddedMonoFont::kFallbackCodepoint) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
return EmbeddedMonoFont::glyph_count();
|
||||
}
|
||||
|
||||
/// The plot library's own source root, so the generator test can run the committed
|
||||
/// script against the committed output. Injected by tests/CMakeLists.txt.
|
||||
constexpr const char* kSourceDir = PLOT_SOURCE_DIR;
|
||||
|
||||
} // namespace
|
||||
|
||||
// The embedded atlas' metrics: a line taller than its ascent (descenders need the
|
||||
// room), a non-empty advance, and -- the point of the face -- a fixed advance, so
|
||||
// a label's width is its length and nothing else. A text backend with variable
|
||||
// advances is the tile the plot library deliberately does not lay out (see the
|
||||
// spec's non-goals), so `i` and `W` measure the same and a caller cannot be
|
||||
// surprised by a label's reserved strip.
|
||||
PLOT_TEST(embedded_font_metrics_are_sane) {
|
||||
const EmbeddedMonoFont f;
|
||||
PLOT_CHECK(f.height() > f.ascent());
|
||||
PLOT_CHECK(f.ascent() > 0.0f);
|
||||
PLOT_CHECK_NEAR(f.measure(""), 0.0, 1e-6);
|
||||
PLOT_CHECK(f.measure("ab") < f.measure("abc")); // monotonic in length
|
||||
PLOT_CHECK(f.measure("iiii") < f.measure("WWWW")); // proportional-ish, not fixed-width-blank
|
||||
PLOT_CHECK_NEAR(f.measure(nullptr), 0.0, 1e-6);
|
||||
// One advance per codepoint, whatever the glyph: this is `i` and `W` alike.
|
||||
PLOT_CHECK_NEAR(f.measure("i"), f.measure("W"), 1e-6);
|
||||
PLOT_CHECK_NEAR(f.measure("i"), EmbeddedMonoFont::advance_codepoint('i'), 1e-6);
|
||||
// Monotone in length, and exactly the sum of the advances it stages.
|
||||
PLOT_CHECK(f.measure("ab") < f.measure("abc"));
|
||||
PLOT_CHECK(f.measure("abc") < f.measure("abcd"));
|
||||
PLOT_CHECK_NEAR(f.measure("12.5 ms"),
|
||||
(f.glyph('1')->advance + f.glyph('2')->advance + f.glyph('.')->advance +
|
||||
f.glyph('5')->advance + f.glyph(' ')->advance + f.glyph('m')->advance +
|
||||
f.glyph('s')->advance),
|
||||
1e-6);
|
||||
// The em is the size the table was baked at, and the scale the renderer draws
|
||||
// with is a ratio of it -- so it must be a positive number and not `height()`.
|
||||
PLOT_CHECK_NEAR(f.em_px(), static_cast<plot::f32>(EmbeddedMonoFont::kEmPx), 1e-6);
|
||||
PLOT_CHECK(f.em_px() > 0.0f);
|
||||
}
|
||||
|
||||
PLOT_TEST(bitmap_font_advance_follows_ink_extent) {
|
||||
plot::render::BitmapFont f;
|
||||
PLOT_CHECK(f.advance('i') < f.advance('W'));
|
||||
// Whitespace is a blank glyph with a nonzero advance, never zero.
|
||||
PLOT_CHECK(f.advance(' ') >= 2.0f);
|
||||
// Advance is ink width + 1px of spacing, floored at 2px: derived here from
|
||||
// the same bitmaps the atlas uploads, so a second width table cannot drift.
|
||||
auto ink_width = [&f](char c) {
|
||||
const plot::u8* rows = f.glyph_bitmap(c);
|
||||
int lo = 8;
|
||||
int hi = -1;
|
||||
for (plot::u32 y = 0; y < f.glyph_rows(c); ++y) {
|
||||
for (int x = 0; x < 8; ++x) {
|
||||
if ((rows[y] >> x) & 1u) {
|
||||
if (x < lo) lo = x;
|
||||
if (x > hi) hi = x;
|
||||
// Every codepoint the atlas claims to cover has its own cell, the cell is inside
|
||||
// the atlas and is the declared cell, and its ink is non-empty -- the generator
|
||||
// refuses to emit a blank cell for a covered codepoint, and this pins that the
|
||||
// committed atlas is the generator's output and not a hand edit. U+0020 is the
|
||||
// one documented blank: a space has no ink by definition and still advances.
|
||||
PLOT_TEST(embedded_atlas_covers_every_expected_codepoint) {
|
||||
const EmbeddedMonoFont f;
|
||||
const plot::Vec2 cell = f.cell_uv();
|
||||
PLOT_CHECK_NEAR(cell.x, static_cast<plot::f32>(EmbeddedMonoFont::cell_width()) /
|
||||
static_cast<plot::f32>(f.atlas_width()),
|
||||
1e-9);
|
||||
PLOT_CHECK_NEAR(cell.y, static_cast<plot::f32>(EmbeddedMonoFont::cell_height()) /
|
||||
static_cast<plot::f32>(f.atlas_height()),
|
||||
1e-9);
|
||||
PLOT_CHECK(f.atlas_pixels() != nullptr);
|
||||
|
||||
std::vector<plot::u32> expected;
|
||||
for (plot::u32 cp = 0x20u; cp <= 0x7Eu; ++cp) expected.push_back(cp);
|
||||
for (plot::u32 cp : {0x00B0u, 0x00B5u, 0x03A9u, 0x2013u, 0x2014u}) expected.push_back(cp);
|
||||
for (plot::u32 cp : expected) {
|
||||
const plot::u32 slot = EmbeddedMonoFont::glyph_index(cp);
|
||||
PLOT_CHECK(slot < EmbeddedMonoFont::glyph_count());
|
||||
PLOT_CHECK(EmbeddedMonoFont::codepoint_of_slot(slot) == cp);
|
||||
const plot::render::Glyph* g = f.glyph(cp);
|
||||
PLOT_CHECK(g != nullptr);
|
||||
// The rect is the atlas cell, and it lies inside the atlas.
|
||||
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);
|
||||
if (cp == 0x20u) {
|
||||
PLOT_CHECK(cell_ink_units(f, slot) == 0u); // a space is blank ...
|
||||
PLOT_CHECK(g->advance > 0.0f); // ... and still advances
|
||||
} else {
|
||||
PLOT_CHECK(cell_ink_units(f, slot) > 0u); // a covered glyph has ink
|
||||
}
|
||||
}
|
||||
|
||||
// The fallback box is a real cell with real ink, at a fixed slot, and every
|
||||
// codepoint outside the coverage set draws it -- so a label the atlas cannot
|
||||
// spell is visible rather than silently blank.
|
||||
const plot::u32 fallback = fallback_slot();
|
||||
PLOT_CHECK(fallback < EmbeddedMonoFont::glyph_count());
|
||||
PLOT_CHECK(cell_ink_units(f, fallback) > 0u);
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x4E00u) == fallback);
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0xFFFFu) == fallback);
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(plot::render::kInvalidCodepoint) == fallback);
|
||||
PLOT_CHECK(f.glyph(0x4E00u)->advance > 0.0f);
|
||||
}
|
||||
|
||||
// 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
|
||||
// half of the anti-aliasing evidence (the frame-level half is in the render
|
||||
// smoke test): a table that passed through a threshold would fail it.
|
||||
PLOT_TEST(embedded_atlas_holds_antialiased_coverage) {
|
||||
const EmbeddedMonoFont f;
|
||||
const plot::u8* atlas = f.atlas_pixels();
|
||||
const std::size_t size = static_cast<std::size_t>(f.atlas_width()) * f.atlas_height();
|
||||
plot::u32 partial = 0u;
|
||||
plot::u32 full = 0u;
|
||||
plot::u32 blank = 0u;
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
if (atlas[i] == 0u) ++blank;
|
||||
else if (atlas[i] == 255u) ++full;
|
||||
else ++partial;
|
||||
}
|
||||
return hi < 0 ? 0 : hi - lo + 1;
|
||||
};
|
||||
const char* samples = "iW.1_m";
|
||||
PLOT_CHECK(full > 0u);
|
||||
PLOT_CHECK(blank > 0u);
|
||||
// Every glyph's stroke edge contributes: the partial population is a large
|
||||
// fraction of the inked texels, not a handful of rounding artefacts.
|
||||
PLOT_CHECK(partial > full / 2u);
|
||||
}
|
||||
|
||||
// The face is monospaced, so every codepoint's advance is the same -- but it is
|
||||
// the font's own `hmtx` advance carried through the bake, not a constant invented
|
||||
// here: `advance_codepoint` and the per-glyph `advance` agree, and the whitespace
|
||||
// glyphs that must never be zero are not.
|
||||
PLOT_TEST(embedded_font_advance_matches_the_font_units) {
|
||||
const EmbeddedMonoFont f;
|
||||
const plot::f32 advance = EmbeddedMonoFont::advance_codepoint('A');
|
||||
PLOT_CHECK(advance > 0.0f);
|
||||
PLOT_CHECK(EmbeddedMonoFont::advance_codepoint(' ') > 0.0f);
|
||||
const char* samples = "iW.1_m ";
|
||||
for (const char* c = samples; *c != '\0'; ++c) {
|
||||
const int ink = ink_width(*c);
|
||||
const f32 want = ink + 1 < 2 ? 2.0f : static_cast<f32>(ink + 1);
|
||||
PLOT_CHECK_NEAR(f.advance(*c), want, 1e-6);
|
||||
PLOT_CHECK_NEAR(f.glyph(static_cast<plot::u32>(*c))->advance, advance, 1e-6);
|
||||
}
|
||||
// `advance_codepoint` is a lookup, not a walk: it agrees with `measure` for a
|
||||
// single decoded codepoint, and disagrees with nothing in the table.
|
||||
PLOT_CHECK_NEAR(f.measure("W"), EmbeddedMonoFont::advance_codepoint('W'), 1e-6);
|
||||
for (plot::u32 cp = 0x20u; cp <= 0x7Eu; ++cp) {
|
||||
PLOT_CHECK_NEAR(f.glyph(cp)->advance, EmbeddedMonoFont::advance_codepoint(cp), 1e-6);
|
||||
}
|
||||
// measure() is exactly the sum of the glyph advances it stages.
|
||||
f32 sum = 0.0f;
|
||||
for (const char* c = "12.5 ms"; *c != '\0'; ++c) sum += f.advance(*c);
|
||||
PLOT_CHECK_NEAR(f.measure("12.5 ms"), sum, 1e-6);
|
||||
}
|
||||
|
||||
PLOT_TEST(bitmap_font_decodes_si_units_and_falls_back_visibly) {
|
||||
plot::render::BitmapFont f;
|
||||
// plot::format_si emits U+00B5 MICRO SIGN, U+00B0 DEGREE SIGN and U+03A9 OHM.
|
||||
// Decoding and the fallback, on the codepoints `plot::format_si` and a plotting
|
||||
// label actually emit: the three SI symbols are real glyphs, everything else is
|
||||
// the visible fallback box, and a malformed byte is not measured as zero.
|
||||
PLOT_TEST(embedded_font_decodes_si_units_and_falls_back_visibly) {
|
||||
const EmbeddedMonoFont f;
|
||||
const plot::u32 fallback = fallback_slot();
|
||||
const f32 micro = f.measure("\xC2\xB5s");
|
||||
const f32 degree = f.measure("\xC2\xB0");
|
||||
const f32 ohm = f.measure("\xCE\xA9");
|
||||
PLOT_CHECK(micro > 0.0f);
|
||||
PLOT_CHECK(degree > 0.0f);
|
||||
PLOT_CHECK(ohm > 0.0f);
|
||||
PLOT_CHECK_NEAR(micro, f.advance_codepoint(0x00B5u) + f.advance('s'), 1e-6);
|
||||
PLOT_CHECK_NEAR(degree, f.advance_codepoint(0x00B0u), 1e-6);
|
||||
PLOT_CHECK_NEAR(ohm, f.advance_codepoint(0x03A9u), 1e-6);
|
||||
// Those three map to their own renderable glyph, not to the missing-glyph box.
|
||||
const plot::u32 fallback = f.glyph_index(0x4E00u); // CJK, deliberately absent
|
||||
PLOT_CHECK(f.glyph_index(0x00B5u) != fallback);
|
||||
PLOT_CHECK(f.glyph_index(0x00B0u) != fallback);
|
||||
PLOT_CHECK(f.glyph_index(0x03A9u) != fallback);
|
||||
PLOT_CHECK(f.glyph_index(0x00B5u) != f.glyph_index(0x00B0u));
|
||||
PLOT_CHECK(f.glyph_index(0x00B0u) != f.glyph_index(0x03A9u));
|
||||
// Anything else non-ASCII is a visible box with a nonzero advance, and a
|
||||
// malformed UTF-8 byte is treated the same way rather than measured as zero.
|
||||
PLOT_CHECK_NEAR(f.measure("\xE4\xB8\x80"), f.advance_codepoint(0x4E00u), 1e-6);
|
||||
PLOT_CHECK(f.advance_codepoint(0x4E00u) > 0.0f);
|
||||
PLOT_CHECK(f.advance(static_cast<char>(0x80)) > 0.0f);
|
||||
PLOT_CHECK_NEAR(f.advance(static_cast<char>(0x80)), f.advance_codepoint(0x4E00u), 1e-6);
|
||||
// The fallback box glyph has ink; a blank bitmap would hide the problem.
|
||||
bool any_ink = false;
|
||||
const plot::u8* box = plot::render::BitmapFont::glyph_bitmap_of(fallback);
|
||||
for (plot::u32 y = 0; y < plot::render::BitmapFont::kGlyphPx; ++y) {
|
||||
any_ink = any_ink || box[y] != 0;
|
||||
}
|
||||
PLOT_CHECK(any_ink);
|
||||
PLOT_CHECK_NEAR(micro, EmbeddedMonoFont::advance_codepoint(0x00B5u) + f.glyph('s')->advance, 1e-6);
|
||||
// Each maps to its own cell, not to the box.
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x00B5u) != fallback);
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x00B0u) != fallback);
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x03A9u) != fallback);
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x00B5u) != EmbeddedMonoFont::glyph_index(0x00B0u));
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x00B0u) != EmbeddedMonoFont::glyph_index(0x03A9u));
|
||||
// The two dashes the coverage adds are real cells too, not the box.
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x2013u) != fallback);
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(0x2014u) != fallback);
|
||||
// A CJK codepoint and a bare malformed byte both draw the box, and both
|
||||
// advance: a missing glyph is visible and never a zero-width hole.
|
||||
PLOT_CHECK_NEAR(f.measure("\xE4\xB8\x80"), EmbeddedMonoFont::advance_codepoint(0x4E00u), 1e-6);
|
||||
PLOT_CHECK_NEAR(f.measure("\x80"), EmbeddedMonoFont::advance_codepoint(0x4E00u), 1e-6);
|
||||
}
|
||||
|
||||
// The packed atlas must be left-to-right in absolute terms: `(row >> x) & 1` is
|
||||
// also how every other expectation here is derived, so a table transcribed with
|
||||
// the opposite in-row bit order would keep those tests green while drawing
|
||||
// mirrored text. 'A' cannot catch that (it is
|
||||
// symmetric about its vertical axis); 'L', 'b' and '/' can.
|
||||
PLOT_TEST(bitmap_font_bit_order_runs_left_to_right) {
|
||||
using plot::render::BitmapFont;
|
||||
const plot::u8* atlas = BitmapFont::atlas_image();
|
||||
auto ink = [atlas](char c, plot::u32 col, plot::u32 row) {
|
||||
const plot::u32 slot = static_cast<plot::u32>(c) - BitmapFont::kFirstChar;
|
||||
const plot::Vec2 origin = BitmapFont::atlas_origin(slot);
|
||||
const auto x = static_cast<std::size_t>(origin.x) + col;
|
||||
const auto y = static_cast<std::size_t>(origin.y) + row;
|
||||
return atlas[y * BitmapFont::kAtlasWidth + x] != 0u;
|
||||
// The atlas must be left-to-right in absolute terms. The coverage bytes are what
|
||||
// the GPU samples, so a generator that rasterised with the opposite x sense -- or
|
||||
// an atlas packed with the cells reversed -- would keep every rect check green
|
||||
// while drawing mirrored text. 'A' cannot catch that (it is symmetric about its
|
||||
// vertical axis); 'L', 'b' and '/' can, and they are asserted against absolute
|
||||
// texel coordinates rather than against the table itself.
|
||||
PLOT_TEST(embedded_atlas_orientation_runs_left_to_right) {
|
||||
const EmbeddedMonoFont f;
|
||||
using plot::render::EmbeddedMonoFont;
|
||||
auto ink = [&f](char c, plot::u32 col, plot::u32 row) {
|
||||
const plot::u32 slot = EmbeddedMonoFont::glyph_index(static_cast<plot::u32>(c));
|
||||
return cell_texel(f, slot, col, row) != 0u;
|
||||
};
|
||||
// 'L': stem and top serif on the left, the foot running right along the
|
||||
// bottom row and stopping one texel short of the right edge.
|
||||
PLOT_CHECK(ink('L', 0u, 0u));
|
||||
PLOT_CHECK(!ink('L', 7u, 0u));
|
||||
PLOT_CHECK(ink('L', 1u, 3u));
|
||||
PLOT_CHECK(!ink('L', 6u, 3u));
|
||||
PLOT_CHECK(ink('L', 6u, 6u));
|
||||
PLOT_CHECK(!ink('L', 7u, 6u));
|
||||
// 'b': the stem is the leftmost column of the upper rows.
|
||||
PLOT_CHECK(ink('b', 0u, 0u));
|
||||
PLOT_CHECK(!ink('b', 7u, 0u));
|
||||
// '/': starts at the right of the top row and ends at the left of the last
|
||||
// one — the opposite of '\'.
|
||||
PLOT_CHECK(ink('/', 6u, 0u));
|
||||
PLOT_CHECK(!ink('/', 0u, 0u));
|
||||
PLOT_CHECK(ink('/', 0u, 6u));
|
||||
PLOT_CHECK(!ink('/', 6u, 6u));
|
||||
// 'L': the stem runs down the left, and the foot runs right along the bottom --
|
||||
// so at cap height there is ink on the left and none on the right, and the
|
||||
// bottom row has ink to the right of where the stem stopped.
|
||||
PLOT_CHECK(ink('L', 1u, 6u));
|
||||
PLOT_CHECK(!ink('L', 6u, 6u));
|
||||
PLOT_CHECK(ink('L', 5u, 11u));
|
||||
PLOT_CHECK(!ink('L', 5u, 3u));
|
||||
// 'b': the stem is the left column of the upper rows, the bowl the right.
|
||||
PLOT_CHECK(ink('b', 1u, 4u));
|
||||
PLOT_CHECK(!ink('b', 6u, 4u));
|
||||
PLOT_CHECK(ink('b', 6u, 8u));
|
||||
// '/': starts at the top right and ends at the bottom left -- the opposite of
|
||||
// '\'. This is the pair that pins the diagonal's direction.
|
||||
PLOT_CHECK(ink('/', 5u, 2u));
|
||||
PLOT_CHECK(!ink('/', 2u, 2u));
|
||||
PLOT_CHECK(ink('/', 2u, 12u));
|
||||
PLOT_CHECK(!ink('/', 5u, 12u));
|
||||
}
|
||||
|
||||
PLOT_TEST(bitmap_font_atlas_is_uploadable) {
|
||||
using plot::render::BitmapFont;
|
||||
PLOT_CHECK(BitmapFont::kAtlasWidth == BitmapFont::kAtlasCols * BitmapFont::kGlyphPx);
|
||||
PLOT_CHECK(BitmapFont::kAtlasHeight % BitmapFont::kGlyphPx == 0);
|
||||
PLOT_CHECK(BitmapFont::kAtlasCols * BitmapFont::kAtlasRows >= BitmapFont::kGlyphCount);
|
||||
PLOT_CHECK(BitmapFont::kAsciiGlyphs == 95);
|
||||
// The atlas is a fixed-cell grid of one texel format: a whole number of cells
|
||||
// across, the cell dividing the height, and every glyph's rect the declared cell
|
||||
// -- so the pipeline's single per-draw rect size is the right one for every glyph.
|
||||
PLOT_TEST(embedded_atlas_is_uploadable) {
|
||||
using plot::render::EmbeddedMonoFont;
|
||||
PLOT_CHECK(EmbeddedMonoFont::cell_width() > 0u);
|
||||
PLOT_CHECK(EmbeddedMonoFont::cell_height() > 0u);
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_count() == 95u + 5u + 1u); // ASCII, SI/dashes, box
|
||||
|
||||
plot::render::BitmapFont f;
|
||||
PLOT_CHECK(f.glyph_rows('A') == 8);
|
||||
const EmbeddedMonoFont f;
|
||||
PLOT_CHECK(f.atlas_width() == EmbeddedMonoFont::cell_width() * EmbeddedMonoFont::glyph_count());
|
||||
PLOT_CHECK(f.atlas_height() == EmbeddedMonoFont::cell_height());
|
||||
PLOT_CHECK(f.atlas_pixels() != nullptr);
|
||||
PLOT_CHECK(f.height() > 0.0f);
|
||||
PLOT_CHECK(f.ascent() > 0.0f);
|
||||
PLOT_CHECK(f.ascent() <= f.height());
|
||||
PLOT_CHECK(f.ascent() < f.height());
|
||||
|
||||
// Each glyph occupies its own atlas cell, and that cell is the bitmap byte
|
||||
// for byte: 255 where a row bit is set, 0 elsewhere (straight alpha).
|
||||
const plot::u8* atlas = BitmapFont::atlas_image();
|
||||
const plot::u32 slots[] = {'A' - 32, 'g' - 32, 'i' - 32, 'W' - 32, 0, '.' - 32,
|
||||
BitmapFont::kFallbackSlot, BitmapFont::kMicroSlot};
|
||||
for (const plot::u32 slot : slots) {
|
||||
const plot::Vec2 o = BitmapFont::atlas_origin(slot);
|
||||
const plot::u8* rows = BitmapFont::glyph_bitmap_of(slot);
|
||||
for (plot::u32 y = 0; y < BitmapFont::kGlyphPx; ++y) {
|
||||
for (plot::u32 x = 0; x < BitmapFont::kGlyphPx; ++x) {
|
||||
const plot::u8 want = ((rows[y] >> x) & 1u) != 0u ? 0xFFu : 0x00u;
|
||||
const auto row = static_cast<std::size_t>(o.y) + y;
|
||||
const auto col = static_cast<std::size_t>(o.x) + x;
|
||||
PLOT_CHECK(atlas[row * BitmapFont::kAtlasWidth + col] == want);
|
||||
// Every cell, the fallback included, is the declared cell and holds at least
|
||||
// one non-zero byte (bar the space), and the ASCII codepoints are a direct
|
||||
// index into the table.
|
||||
for (plot::u32 slot = 0; slot < EmbeddedMonoFont::glyph_count(); ++slot) {
|
||||
const plot::u32 cp = EmbeddedMonoFont::codepoint_of_slot(slot);
|
||||
if (cp != EmbeddedMonoFont::kFallbackCodepoint) {
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(cp) == slot);
|
||||
}
|
||||
const plot::render::Glyph* g = f.glyph(cp);
|
||||
PLOT_CHECK(plot::render::glyph_is_cell(*g, f.cell_uv()));
|
||||
}
|
||||
for (plot::u32 cp = 0x20u; cp <= 0x7Eu; ++cp) {
|
||||
PLOT_CHECK(EmbeddedMonoFont::glyph_index(cp) == cp - 0x20u);
|
||||
}
|
||||
// The atlas is a whole number of cells and no glyph leaves its cell.
|
||||
PLOT_CHECK(BitmapFont::kAtlasWidth == 16u * BitmapFont::kGlyphPx);
|
||||
// A single-row atlas: the cell spans the whole height, so v is 0..1.
|
||||
PLOT_CHECK_NEAR(f.cell_uv().y, 1.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(EmbeddedMonoFont::atlas_origin(0u).x, 0.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(EmbeddedMonoFont::atlas_origin(1u).x,
|
||||
static_cast<plot::f32>(EmbeddedMonoFont::cell_width()), 1e-9);
|
||||
}
|
||||
|
||||
// The committed atlas is the generator's output, not a hand edit, and the
|
||||
// generator is deterministic: the same source font, em size and supersample
|
||||
// factor produce it byte for byte. The check runs the committed script against
|
||||
// the committed file, so a table touched by hand, a stale bake after a coverage
|
||||
// change, or a non-deterministic generator all fail here.
|
||||
//
|
||||
// `fontTools` is an authoring-time tool and the test machine is not required to
|
||||
// have it. Its absence is *reported*, not silently passed: the generator exits
|
||||
// with a distinct code for it, and this case says so on stdout.
|
||||
PLOT_TEST(generator_is_deterministic_and_current) {
|
||||
const std::string command = std::string("python3 ") + kSourceDir +
|
||||
"/tools/fonts/gen_atlas.py --check --out " + kSourceDir +
|
||||
"/src/render/embedded_mono_font.cpp";
|
||||
const int status = std::system(command.c_str());
|
||||
PLOT_CHECK(status != -1);
|
||||
PLOT_CHECK(WIFEXITED(status));
|
||||
const int code = WEXITSTATUS(status);
|
||||
if (code == 3) { // `gen_atlas.py`'s "fontTools is not installed" code
|
||||
std::printf(
|
||||
"SKIPPED: generator_is_deterministic_and_current -- fontTools is not "
|
||||
"installed, so the atlas could not be re-baked\n");
|
||||
return;
|
||||
}
|
||||
PLOT_CHECK(code == 0);
|
||||
}
|
||||
|
||||
PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
|
||||
plot::render::BitmapFont f;
|
||||
plot::render::EmbeddedMonoFont f;
|
||||
plot::render::TextBatch b;
|
||||
b.begin();
|
||||
const plot::Vec2 end = b.add_text("12.5 ms", {0.0f, 0.0f}, 1.0f, f);
|
||||
@@ -254,7 +400,7 @@ PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
|
||||
PLOT_CHECK_NEAR(end.y, 0.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(b.data()[0], 0.0, 1e-6); // first cell at the pen
|
||||
PLOT_CHECK_NEAR(b.data()[1], 0.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(b.data()[6], f.advance('1'), 1e-6); // pen advanced per glyph
|
||||
PLOT_CHECK_NEAR(b.data()[6], f.glyph('1')->advance, 1e-6); // pen advanced per glyph
|
||||
// The staged cell's UV is the glyph's own atlas rect (see `Glyph`), and the
|
||||
// basis of an unrotated run is the screen's +x. '.' is the string's third
|
||||
// glyph, so its instance is the third: floats 12..17.
|
||||
@@ -284,7 +430,7 @@ PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
|
||||
PLOT_CHECK(b.glyph_count() == 2);
|
||||
PLOT_CHECK_NEAR(scaled.x, 10.0f + 2.0f * f.measure("AW"), 1e-6);
|
||||
PLOT_CHECK_NEAR(scaled.y, 4.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(b.data()[6], 10.0f + 2.0f * f.advance('A'), 1e-6); // second cell's pen
|
||||
PLOT_CHECK_NEAR(b.data()[6], 10.0f + 2.0f * f.glyph('A')->advance, 1e-6); // second cell's pen
|
||||
}
|
||||
|
||||
// The pen follows the basis, not the screen axes. A run staged with `axis` has to
|
||||
@@ -292,7 +438,7 @@ PLOT_TEST(text_batch_stages_one_quad_per_glyph) {
|
||||
// what reserves the room for a rotated y label -- and its cells have to march along
|
||||
// the same direction, or the ink lands beside the pen instead of up from it.
|
||||
PLOT_TEST(text_batch_advances_along_its_basis) {
|
||||
plot::render::BitmapFont f;
|
||||
plot::render::EmbeddedMonoFont f;
|
||||
plot::render::TextBatch b;
|
||||
const plot::Vec2 pen{100.0f, 40.0f};
|
||||
const f32 width = f.measure("12.5 ms");
|
||||
@@ -318,7 +464,7 @@ PLOT_TEST(text_batch_advances_along_its_basis) {
|
||||
// The second cell is one advance further up than the pen, which is the same rule
|
||||
// the unrotated case follows in x.
|
||||
PLOT_CHECK_NEAR(b.data()[6], pen.x, 1e-6);
|
||||
PLOT_CHECK_NEAR(b.data()[7], pen.y - f.advance('1'), 1e-6);
|
||||
PLOT_CHECK_NEAR(b.data()[7], pen.y - f.glyph('1')->advance, 1e-6);
|
||||
|
||||
// A diagonal direction is normalised, so a caller can hand over any direction
|
||||
// and the basis the shader receives is still unit.
|
||||
@@ -340,6 +486,7 @@ class TrimmedCellBackend final : public plot::render::TextBackend {
|
||||
plot::f32 measure(const char*) const override { return 8.0f; }
|
||||
plot::f32 height() const override { return 8.0f; }
|
||||
plot::f32 ascent() const override { return 8.0f; }
|
||||
plot::f32 em_px() const override { return 8.0f; }
|
||||
plot::Vec2 cell_uv() const override { return plot::Vec2{0.5f, 0.25f}; }
|
||||
plot::u32 revision() const override { return 1u; } // baked once, never re-baked
|
||||
const plot::render::Glyph* glyph(plot::u32 codepoint) const override {
|
||||
@@ -396,17 +543,19 @@ PLOT_TEST(a_trimmed_glyph_is_not_the_atlas_cell) {
|
||||
PLOT_CHECK(plot::render::glyph_is_cell(rounded, cell));
|
||||
|
||||
// A real font's grid holds for every glyph it publishes, not just the first.
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
for (plot::u32 codepoint = 32u; codepoint < 127u; ++codepoint) {
|
||||
const plot::render::Glyph* g = font.glyph(codepoint);
|
||||
PLOT_CHECK(g != nullptr);
|
||||
PLOT_CHECK(plot::render::glyph_is_cell(*g, font.cell_uv()));
|
||||
}
|
||||
PLOT_CHECK_NEAR(font.cell_uv().x, static_cast<plot::f32>(plot::render::BitmapFont::kGlyphPx) /
|
||||
static_cast<plot::f32>(plot::render::BitmapFont::kAtlasWidth),
|
||||
PLOT_CHECK_NEAR(font.cell_uv().x,
|
||||
static_cast<plot::f32>(plot::render::EmbeddedMonoFont::cell_width()) /
|
||||
static_cast<plot::f32>(font.atlas_width()),
|
||||
1e-9);
|
||||
PLOT_CHECK_NEAR(font.cell_uv().y, static_cast<plot::f32>(plot::render::BitmapFont::kGlyphPx) /
|
||||
static_cast<plot::f32>(plot::render::BitmapFont::kAtlasHeight),
|
||||
PLOT_CHECK_NEAR(font.cell_uv().y,
|
||||
static_cast<plot::f32>(plot::render::EmbeddedMonoFont::cell_height()) /
|
||||
static_cast<plot::f32>(font.atlas_height()),
|
||||
1e-9);
|
||||
}
|
||||
|
||||
@@ -418,6 +567,7 @@ class HalfCellBackend final : public plot::render::TextBackend {
|
||||
plot::f32 measure(const char*) const override { return 4.0f; }
|
||||
plot::f32 height() const override { return 8.0f; }
|
||||
plot::f32 ascent() const override { return 6.0f; }
|
||||
plot::f32 em_px() const override { return 8.0f; }
|
||||
plot::Vec2 cell_uv() const override { return plot::Vec2{0.5f, 0.25f}; }
|
||||
plot::u32 revision() const override { return 1u; }
|
||||
const plot::render::Glyph* glyph(plot::u32) const override { return &cell_; }
|
||||
@@ -471,7 +621,7 @@ PLOT_TEST(text_batch_takes_its_cell_from_the_backend) {
|
||||
|
||||
// And the real font's grid, staged the same way, reports the font's cell -- the
|
||||
// two never mix silently, and the batch's value tracks the backend it was given.
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
b.begin();
|
||||
b.add_text("A", {0.0f, 0.0f}, 1.0f, font);
|
||||
PLOT_CHECK_NEAR(b.glyph_cell_uv().x, font.cell_uv().x, 1e-9);
|
||||
@@ -503,6 +653,7 @@ class HoleInCoverageBackend final : public plot::render::TextBackend {
|
||||
}
|
||||
plot::f32 height() const override { return 8.0f; }
|
||||
plot::f32 ascent() const override { return 6.0f; }
|
||||
plot::f32 em_px() const override { return 8.0f; }
|
||||
plot::Vec2 cell_uv() const override { return plot::Vec2{0.5f, 1.0f}; }
|
||||
plot::u32 revision() const override { return 1u; }
|
||||
const plot::render::Glyph* glyph(plot::u32 codepoint) const override {
|
||||
@@ -523,7 +674,7 @@ class HoleInCoverageBackend final : public plot::render::TextBackend {
|
||||
// it and does not advance the pen, so a run stays exactly as long as the same
|
||||
// backend measured it and the glyphs it *can* draw keep their places. A backend
|
||||
// that would rather show something publishes a fallback box as that codepoint's
|
||||
// glyph (which is what `BitmapFont` does); this is the other legal answer, and the
|
||||
// glyph (which is what `EmbeddedMonoFont` does); this is the other legal answer, and the
|
||||
// staging path has to honour it rather than draw whatever was staged last or
|
||||
// advance by a width nobody published.
|
||||
PLOT_TEST(add_text_skips_a_codepoint_the_backend_cannot_draw) {
|
||||
@@ -568,7 +719,7 @@ PLOT_TEST(add_text_skips_a_codepoint_the_backend_cannot_draw) {
|
||||
// A real font never hits this path (every codepoint maps to its box), so the
|
||||
// same string through it stages one glyph per codepoint: the two answers to
|
||||
// "no glyph" are the backend's choice, not the pipeline's.
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
batch.begin();
|
||||
batch.add_text("A?B", pen, 1.0f, font);
|
||||
PLOT_CHECK(batch.glyph_count() == 3);
|
||||
|
||||
+81
-48
@@ -119,6 +119,24 @@ bool pixel_is(const plot::u8 px[4], plot::Color c) {
|
||||
return near(px[0], c.r) && near(px[1], c.g) && near(px[2], c.b);
|
||||
}
|
||||
|
||||
/// True when `px` holds `c` at some coverage -- the anti-aliased case `pixel_is`
|
||||
/// cannot see. The embedded atlas is greyscale and the renderer draws it at
|
||||
/// `Style::axis_font_px` from a 13 px bake, so at the default size a glyph stroke
|
||||
/// is barely a pixel wide and *no* pixel is ever fully covered: demanding the
|
||||
/// exact colour finds no text at all.
|
||||
///
|
||||
/// A partially covered pixel is `c` scaled toward the frame's dark chrome, so this
|
||||
/// asks for every channel to be at least a third of `c`'s: bright enough to be
|
||||
/// glyph ink, dark enough to exclude a background, and deliberately blind to which
|
||||
/// of two chalk-toned chrome colours a pixel is -- which is why every caller scans
|
||||
/// a region only one element draws in.
|
||||
bool text_ink(const plot::u8 px[4], plot::Color c) {
|
||||
const auto lifted = [](plot::u8 got, plot::f32 want) {
|
||||
return static_cast<plot::f32>(got) >= want * 255.0f * 0.35f;
|
||||
};
|
||||
return lifted(px[0], c.r) && lifted(px[1], c.g) && lifted(px[2], c.b);
|
||||
}
|
||||
|
||||
/// No column held ink.
|
||||
constexpr plot::u32 kNoColumn = 0xFFFFFFFFu;
|
||||
|
||||
@@ -132,6 +150,7 @@ class OnePxPerStringBackend final : public plot::render::TextBackend {
|
||||
plot::f32 measure(const char*) const override { return 1.0f; }
|
||||
plot::f32 height() const override { return 1.0f; }
|
||||
plot::f32 ascent() const override { return 1.0f; }
|
||||
plot::f32 em_px() const override { return 1.0f; }
|
||||
const plot::render::Glyph* glyph(plot::u32) const override { return nullptr; }
|
||||
plot::Vec2 cell_uv() const override { return plot::Vec2{}; } // no atlas, no cell
|
||||
const plot::u8* atlas_pixels() const override { return nullptr; }
|
||||
@@ -190,6 +209,8 @@ class BakeableBackend final : public plot::render::TextBackend {
|
||||
plot::f32 measure(const char*) const override { return 8.0f; }
|
||||
plot::f32 height() const override { return static_cast<plot::f32>(size_); }
|
||||
plot::f32 ascent() const override { return static_cast<plot::f32>(size_); }
|
||||
/// Its cell is the whole atlas, so its em is the bake size: one unit is one pixel.
|
||||
plot::f32 em_px() const override { return static_cast<plot::f32>(size_); }
|
||||
const plot::render::Glyph* glyph(plot::u32) const override { return &glyph_; }
|
||||
plot::Vec2 cell_uv() const override { return pixels_.empty() ? plot::Vec2{} : plot::Vec2{1.0f, 1.0f}; }
|
||||
const plot::u8* atlas_pixels() const override { return pixels_.empty() ? nullptr : pixels_.data(); }
|
||||
@@ -217,14 +238,16 @@ struct InkBox {
|
||||
plot::u32 height() const noexcept { return any() ? y1 - y0 + 1u : 0u; }
|
||||
};
|
||||
|
||||
/// Scans the whole framebuffer for fully covered `text`-coloured pixels.
|
||||
/// Scans the whole framebuffer for `text`-coloured ink, anti-aliased fringe and
|
||||
/// all (see `text_ink`). These cases draw nothing but the run, so everything lit
|
||||
/// in the target's colour is the run.
|
||||
InkBox scan_ink_box(plottest::HeadlessContext& ctx, plot::Color text) {
|
||||
InkBox box;
|
||||
for (plot::u32 y = 0; y < ctx.height; ++y) {
|
||||
for (plot::u32 x = 0; x < ctx.width; ++x) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - y, px));
|
||||
if (!pixel_is(px, text)) continue;
|
||||
if (!text_ink(px, text)) continue;
|
||||
if (!box.any()) {
|
||||
box.x0 = x;
|
||||
box.y0 = y;
|
||||
@@ -290,8 +313,8 @@ struct TextInk {
|
||||
plot::u32 widest_row_run = 0;
|
||||
};
|
||||
|
||||
/// Scans the top-down pixels of [x0, x1) x [y, y + rows) for fully covered text
|
||||
/// pixels in `text`.
|
||||
/// Scans the top-down pixels of [x0, x1) x [y, y + rows) for `text`-coloured ink,
|
||||
/// anti-aliased fringe and all (see `text_ink`).
|
||||
TextInk scan_text_ink(plottest::HeadlessContext& ctx, plot::u32 x0, plot::u32 x1, plot::u32 y,
|
||||
plot::u32 rows, plot::Color text) {
|
||||
TextInk ink;
|
||||
@@ -301,7 +324,7 @@ TextInk scan_text_ink(plottest::HeadlessContext& ctx, plot::u32 x0, plot::u32 x1
|
||||
for (plot::u32 x = x0; x < x1; ++x) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - (y + r), px));
|
||||
if (!pixel_is(px, text)) continue;
|
||||
if (!text_ink(px, text)) continue;
|
||||
if (first == kNoColumn) first = x;
|
||||
last = x;
|
||||
if (ink.leftmost == kNoColumn) ink.leftmost = x;
|
||||
@@ -661,8 +684,12 @@ PLOT_TEST(renderer_measures_the_labels_it_draws) {
|
||||
const plot::u32 y = static_cast<plot::u32>(labels.y);
|
||||
const plot::u32 rows = static_cast<plot::u32>(labels.h);
|
||||
const plot::u32 strip_x = static_cast<plot::u32>(labels.x);
|
||||
// Exactly the strip, and no overhang: the axes' own border line runs down its
|
||||
// right edge through these same rows, and a scanner that reached past the strip
|
||||
// would read that line as label ink -- it is the same chalk-toned colour family
|
||||
// as the text (see `text_ink`).
|
||||
const TextInk ink =
|
||||
scan_text_ink(ctx, strip_x, strip_x + static_cast<plot::u32>(labels.w) + 16u, y, rows, text);
|
||||
scan_text_ink(ctx, strip_x, strip_x + static_cast<plot::u32>(labels.w), y, rows, text);
|
||||
PLOT_CHECK(ink.leftmost != kNoColumn); // labels were drawn
|
||||
PLOT_CHECK(ink.leftmost >= strip_x); // inside their strip
|
||||
PLOT_CHECK(static_cast<plot::f32>(ink.widest_row_run) >= 0.6f * widest); // full width drawn
|
||||
@@ -1017,7 +1044,7 @@ PLOT_TEST(titled_axis_pairs_get_their_own_rows) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(static_cast<plot::u32>(strip.x) + column,
|
||||
ctx.height - 1u - (static_cast<plot::u32>(strip.y) + row), px));
|
||||
has = pixel_is(px, title);
|
||||
has = text_ink(px, title);
|
||||
}
|
||||
if (has && !previous) ++bands;
|
||||
if (has) ++rows;
|
||||
@@ -1261,11 +1288,13 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::render::TextAtlas atlas;
|
||||
const plot::Vec2 pen{300.0f, 180.0f};
|
||||
const plot::f32 px = 2.0f; // 16 px cells: six glyphs is over a hundred pixels
|
||||
const plot::u32 cell = static_cast<plot::u32>(8.0f * px);
|
||||
const plot::f32 px = 2.0f; // one cell is 16 x 34 screen pixels at this scale
|
||||
const plot::u32 cell = plot::render::EmbeddedMonoFont::cell_height() * 2u;
|
||||
const plot::u32 advance_px =
|
||||
static_cast<plot::u32>(font.glyph(static_cast<plot::u32>('M'))->advance * px);
|
||||
|
||||
const InkBox flat = draw_text_run(ctx, atlas, font, "MWMWMW", pen, px, {1.0f, 0.0f});
|
||||
PLOT_CHECK(flat.any());
|
||||
@@ -1273,6 +1302,7 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
|
||||
PLOT_CHECK(flat.x0 >= static_cast<plot::u32>(pen.x));
|
||||
PLOT_CHECK(flat.y0 >= static_cast<plot::u32>(pen.y));
|
||||
PLOT_CHECK(flat.height() <= cell);
|
||||
PLOT_CHECK(flat.width() > 4u * advance_px); // six monospaced glyphs, and then some
|
||||
|
||||
const InkBox up = draw_text_run(ctx, atlas, font, "MWMWMW", pen, px, {0.0f, -1.0f});
|
||||
PLOT_CHECK(up.any());
|
||||
@@ -1281,42 +1311,45 @@ PLOT_TEST(rotated_text_advances_along_its_basis) {
|
||||
PLOT_CHECK(up.width() <= cell); // one cell wide, so nothing marched right
|
||||
PLOT_CHECK(up.x0 >= static_cast<plot::u32>(pen.x));
|
||||
PLOT_CHECK(up.x1 < static_cast<plot::u32>(pen.x) + cell);
|
||||
// Ink above the pen: the first cell starts at the pen and every later one is a
|
||||
// cell further up, so the run's rows run from well above the pen down to the
|
||||
// Ink above the pen: the first cell starts at the pen and every later one is an
|
||||
// advance further up, so the run's rows run from well above the pen down to the
|
||||
// pen's own cell -- and the pen's own cell is the lowest row of ink.
|
||||
PLOT_CHECK(up.y0 + 4u * cell < static_cast<plot::u32>(pen.y));
|
||||
PLOT_CHECK(up.y0 + 4u * advance_px < static_cast<plot::u32>(pen.y));
|
||||
PLOT_CHECK(up.y1 < static_cast<plot::u32>(pen.y) + cell);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// The orientation rule, stated rather than hardcoded: a glyph's texel `(col, row)`
|
||||
// lands at the pen plus `col` steps along the cell's +v axis and `row` steps along
|
||||
// its +x axis. That is the mapping kTextVS implements, so with `axis = (0, -1)` the
|
||||
// 'L' -- whose ink is a stem down its left column and a foot along its bottom row --
|
||||
// has to come out as a top bar with the foot running up the right-hand side.
|
||||
// lands at the pen plus `col` steps along the cell's +x axis (`ux` here) and `row`
|
||||
// steps along its +v axis (the perpendicular). That is the mapping kTextVS
|
||||
// implements, so with `axis = (0, -1)` the 'L' -- whose ink is a stem down its left
|
||||
// columns and a foot along its bottom rows -- has to come out as a bar standing up
|
||||
// from the pen with the foot running right.
|
||||
//
|
||||
// The check is two-sided: the basis-derived corner holds ink and the corner a
|
||||
// mirrored perpendicular would put it in does not, so a glyph flipped about its own
|
||||
// diagonal fails even though it is the same size and shape.
|
||||
// The check is two-sided: the texels the basis says hold ink do, and the texels a
|
||||
// mirrored perpendicular would put that ink in hold none -- so a glyph flipped
|
||||
// about its own diagonal fails even though it is the same size and shape. The
|
||||
// coverage is anti-aliased, so "held ink" is a coverage threshold rather than an
|
||||
// exact white: a texel of the stem is fully covered and a texel of the foot is
|
||||
// half, and both are ink.
|
||||
PLOT_TEST(rotated_glyph_orientation_is_not_mirrored) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::render::TextAtlas atlas;
|
||||
const plot::Vec2 pen{300.0f, 200.0f};
|
||||
const plot::f32 px = 4.0f; // an 8x8 glyph at 32 px: one texel is a 4x4 pixel block
|
||||
const plot::f32 px = 4.0f; // one texel is a 4x4 pixel block
|
||||
const plot::Vec2 ux{0.0f, -1.0f}; // a 90 degree counter-clockwise label
|
||||
const plot::Vec2 uy{-ux.y, ux.x}; // (1, 0): the cell's own +v axis
|
||||
|
||||
// Where the centre of the glyph's texel (col, row) is drawn. Every cell is
|
||||
// `kGlyphPx` texels across, the cell's top-left is the pen because the bitmap
|
||||
// font's bearings are zero, and the rule is the shader's own: the atlas' u runs
|
||||
// along the cell's +x axis (`ux` here, `a_axis` there) and its v along the
|
||||
// perpendicular (`v_axis`), which is what `v_uv = a_uv + corner * u_glyph_uv`
|
||||
// maps corner by corner.
|
||||
// Where the centre of the glyph's texel (col, row) is drawn. The cell's top-left
|
||||
// is the pen (the embedded font bakes its bearings into the cell), and the rule
|
||||
// is the shader's own: the atlas' u runs along the cell's +x axis (`ux` here,
|
||||
// `a_axis` there) and its v along the perpendicular (`v_axis`), which is what
|
||||
// `v_uv = a_uv + corner * u_glyph_uv` maps corner by corner.
|
||||
auto texel_centre = [&](plot::f32 col, plot::f32 row, plot::Vec2 v_axis) {
|
||||
const plot::f32 du = (col + 0.5f) * px;
|
||||
const plot::f32 dv = (row + 0.5f) * px;
|
||||
@@ -1326,28 +1359,28 @@ PLOT_TEST(rotated_glyph_orientation_is_not_mirrored) {
|
||||
plot::u8 pxv[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(static_cast<plot::u32>(at.x),
|
||||
ctx.height - 1u - static_cast<plot::u32>(at.y), pxv));
|
||||
return pixel_is(pxv, plot::Color::hex(0xFFFFFF));
|
||||
return pxv[0] >= 96u; // white tint over black: the pixel *is* the coverage
|
||||
};
|
||||
|
||||
PLOT_CHECK(draw_text_run(ctx, atlas, font, "L", pen, px, ux).any());
|
||||
|
||||
// 'L' is inked at texel (0, 0) -- the stem's top -- and at (6, 6), the far end of
|
||||
// its foot. Both corners are where the basis puts them ...
|
||||
PLOT_CHECK(inked(texel_centre(0.0f, 0.0f, uy)));
|
||||
PLOT_CHECK(inked(texel_centre(6.0f, 6.0f, uy)));
|
||||
// ... and the two *off-diagonal* texels of its ink state the rule the diagonal
|
||||
// pair cannot: the stem meets the foot at (0, 6), which is where the cell's
|
||||
// first texel row and last texel column meet, while (6, 0) is blank. A shader
|
||||
// that swapped the cell's two axes would trade those two, and both of these
|
||||
// would fail -- the diagonal probes would not.
|
||||
PLOT_CHECK(inked(texel_centre(0.0f, 6.0f, uy)));
|
||||
PLOT_CHECK(!inked(texel_centre(6.0f, 0.0f, uy)));
|
||||
// ... and the mirrored basis -- the other perpendicular a shader could build --
|
||||
// would put the stem's top on the opposite side of the pen, where this frame has
|
||||
// no ink at all.
|
||||
// The stem (cell columns 1-2, rows 3-12) and the foot (columns 2-6, rows 11-12)
|
||||
// put ink at these texels ...
|
||||
PLOT_CHECK(inked(texel_centre(2.0f, 4.0f, uy))); // stem, fully covered
|
||||
PLOT_CHECK(inked(texel_centre(2.0f, 11.0f, uy))); // stem meets the foot
|
||||
PLOT_CHECK(inked(texel_centre(5.0f, 11.0f, uy))); // the foot's far end
|
||||
// ... and none at these, which are the cell's blank margin: a shader that
|
||||
// swapped the cell's two axes would put the ink in the wrong half and fail both
|
||||
// sides.
|
||||
PLOT_CHECK(!inked(texel_centre(2.0f, 0.0f, uy))); // above the first row of ink
|
||||
PLOT_CHECK(!inked(texel_centre(6.0f, 4.0f, uy))); // right of the stem at cap height
|
||||
PLOT_CHECK(!inked(texel_centre(0.0f, 4.0f, uy))); // the cell's empty first column
|
||||
// The mirrored perpendicular -- the other basis a shader could build -- would
|
||||
// draw the whole cell on the opposite side of the pen, where this frame has no
|
||||
// ink at all.
|
||||
const plot::Vec2 mirrored{-uy.x, -uy.y};
|
||||
PLOT_CHECK(!inked(texel_centre(0.0f, 0.0f, mirrored)));
|
||||
PLOT_CHECK(!inked(texel_centre(6.0f, 6.0f, mirrored)));
|
||||
PLOT_CHECK(!inked(texel_centre(2.0f, 4.0f, mirrored)));
|
||||
PLOT_CHECK(!inked(texel_centre(5.0f, 11.0f, mirrored)));
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
@@ -1505,7 +1538,7 @@ PLOT_TEST(text_atlas_reports_a_status_when_the_entry_points_are_missing) {
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
PLOT_CHECK(plot::gl::available());
|
||||
|
||||
plot::render::BitmapFont font;
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
plot::render::TextAtlas atlas;
|
||||
{
|
||||
RefuseGetIntegerv refused;
|
||||
@@ -1522,8 +1555,8 @@ PLOT_TEST(text_atlas_reports_a_status_when_the_entry_points_are_missing) {
|
||||
// the Status it got.
|
||||
PLOT_CHECK(atlas.sync(font, &st));
|
||||
PLOT_CHECK(!atlas.empty());
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(plot::render::BitmapFont::kAtlasWidth),
|
||||
1e-6);
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().x, static_cast<plot::f32>(font.atlas_width()), 1e-6);
|
||||
PLOT_CHECK_NEAR(atlas.texel_size().y, static_cast<plot::f32>(font.atlas_height()), 1e-6);
|
||||
PLOT_CHECK(plot::gl::glGetError() == plot::gl::GL_NO_ERROR);
|
||||
ctx.destroy();
|
||||
}
|
||||
Executable
+645
@@ -0,0 +1,645 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bake the embedded mono font's anti-aliased atlas, at authoring time.
|
||||
|
||||
Reads a TrueType/OpenType file and writes a C++ translation unit holding a
|
||||
greyscale coverage atlas, one fixed cell per glyph, plus the per-glyph rect,
|
||||
bearing and advance table the text pipeline stages from.
|
||||
|
||||
tools/fonts/gen_atlas.py [--font PATH] [--em N] [--supersample N] [--out PATH] [--check]
|
||||
|
||||
This is an **authoring-time** tool. `plot_core` and every public header under
|
||||
`include/plot/` stay third-party-free: nothing here runs at build time, the
|
||||
atlas is committed as data, and `fontTools` is never needed to compile or run the
|
||||
library. Regenerating is a deliberate act followed by a commit.
|
||||
|
||||
Rasterisation is ours, not a rasteriser's: the glyph outlines are flattened with
|
||||
`fontTools.pens`, scanline-filled at `supersample` times the em, and box-filtered
|
||||
down to one coverage byte per texel. Nothing shells out.
|
||||
|
||||
Determinism
|
||||
-----------
|
||||
Same font file plus the same em size and supersample factor must produce a
|
||||
byte-identical translation unit -- `--check` depends on it, and so does a
|
||||
reviewable diff. Everything emitted is derived from integers (cell geometry, the
|
||||
font's own advance units) or from a fixed, sorted walk of the coverage list; no
|
||||
timestamp, no set/dict iteration order, no float formatting, no host path beyond
|
||||
the source font's file name.
|
||||
|
||||
Blank ink is a hard failure
|
||||
---------------------------
|
||||
Every requested codepoint must rasterise to at least one inked texel. A silent
|
||||
blank cell is exactly the failure that makes a font swap look like it worked, so
|
||||
the generator exits non-zero instead. The single documented exception is U+0020,
|
||||
which is blank *by definition* -- an intentional blank with a full advance.
|
||||
|
||||
Provenance
|
||||
----------
|
||||
The default face is Adwaita Mono, `AdwaitaMono-Regular.ttf`, released under the
|
||||
SIL Open Font License 1.1 (a copy is committed at `licences/OFL-1.1.txt`). The
|
||||
backend is named neutrally (`EmbeddedMonoFont`) because OFL reserves the font
|
||||
name for modified redistributions. The generated file records the source file
|
||||
name, the font's own name/version strings, the em size, the supersample factor
|
||||
and the exact coverage set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_FONT = "/usr/share/fonts/Adwaita/AdwaitaMono-Regular.ttf"
|
||||
DEFAULT_EM = 13
|
||||
DEFAULT_SUPERSAMPLE = 4
|
||||
DEFAULT_OUT = "src/render/embedded_mono_font.cpp"
|
||||
|
||||
SCRIPT = "tools/fonts/gen_atlas.py"
|
||||
|
||||
# The fallback box, in font units with the origin at the pen on the baseline, y up
|
||||
# like the outlines it stands in for. Its two contours wind oppositely, so the
|
||||
# non-zero fill leaves a hollow box: an outer rectangle and an inner hole, each
|
||||
# stroke 100 units (1.3 px at 13 px em). It is authored here, not taken from the
|
||||
# font -- it is what an uncovered codepoint draws.
|
||||
FALLBACK_OUTER = ((60.0, -100.0), (580.0, -100.0), (580.0, 700.0), (60.0, 700.0))
|
||||
FALLBACK_INNER = ((160.0, 0.0), (160.0, 600.0), (480.0, 600.0), (480.0, 0.0))
|
||||
|
||||
# The codepoint side of the coverage set: printable ASCII, the three codepoints
|
||||
# `plot::format_si` emits, and the two dashes a plotting label uses. The fallback
|
||||
# slot is appended after them.
|
||||
ASCII = list(range(0x20, 0x7F))
|
||||
SPECIALS = [0xB0, 0xB5, 0x03A9, 0x2013, 0x2014]
|
||||
|
||||
FALLBACK_CODEPOINT = 0xFFFFFFFF # not a Unicode scalar; cannot collide
|
||||
|
||||
# Sub-curve flattening steps. Fixed, not tolerance-adaptive: at a 13 px em they
|
||||
# are far below one supersampled texel even at 4x, and a constant keeps the
|
||||
# output independent of anything but the font file.
|
||||
QUAD_STEPS = 12
|
||||
CUBIC_STEPS = 16
|
||||
|
||||
EXIT_OK = 0
|
||||
EXIT_MISMATCH = 1
|
||||
EXIT_BAD_FONT = 2
|
||||
EXIT_NO_FONTTOOLS = 3
|
||||
|
||||
|
||||
def _fail(code: int, message: str) -> int:
|
||||
print(f"{SCRIPT}: {message}", file=sys.stderr)
|
||||
return code
|
||||
|
||||
|
||||
def _require_fonttools():
|
||||
try:
|
||||
from fontTools.pens.basePen import BasePen # noqa: F401
|
||||
from fontTools.ttLib import TTFont # noqa: F401
|
||||
except ImportError:
|
||||
raise SystemExit(
|
||||
_fail(
|
||||
EXIT_NO_FONTTOOLS,
|
||||
"fontTools is not installed; this is an authoring-time tool. "
|
||||
"Install it (pip install fonttools) to regenerate the atlas.",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _contour_pen_class():
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
class ContourPen(BasePen):
|
||||
"""Flattens one glyph to contour point lists in cell space.
|
||||
|
||||
`xf` maps a font-unit point (y up, origin at the pen on the baseline) to
|
||||
supersampled cell coordinates (y down, origin at the cell's top-left),
|
||||
so every geometric decision below is made once, here.
|
||||
"""
|
||||
|
||||
def __init__(self, glyph_set, xf):
|
||||
super().__init__(glyph_set)
|
||||
self._xf = xf
|
||||
self.contours = []
|
||||
self._current = []
|
||||
|
||||
def _map(self, point):
|
||||
return self._xf(point[0], point[1])
|
||||
|
||||
def _moveTo(self, pt):
|
||||
self._current = [self._map(pt)]
|
||||
|
||||
def _lineTo(self, pt):
|
||||
self._current.append(self._map(pt))
|
||||
|
||||
def _qCurveToOne(self, control, point):
|
||||
p0 = self._current[-1]
|
||||
p1 = self._map(control)
|
||||
p2 = self._map(point)
|
||||
for step in range(1, QUAD_STEPS + 1):
|
||||
t = step / QUAD_STEPS
|
||||
u = 1.0 - t
|
||||
self._current.append(
|
||||
(
|
||||
u * u * p0[0] + 2.0 * u * t * p1[0] + t * t * p2[0],
|
||||
u * u * p0[1] + 2.0 * u * t * p1[1] + t * t * p2[1],
|
||||
)
|
||||
)
|
||||
|
||||
def _curveToOne(self, c1, c2, point):
|
||||
p0 = self._current[-1]
|
||||
p1 = self._map(c1)
|
||||
p2 = self._map(c2)
|
||||
p3 = self._map(point)
|
||||
for step in range(1, CUBIC_STEPS + 1):
|
||||
t = step / CUBIC_STEPS
|
||||
u = 1.0 - t
|
||||
b0 = u * u * u
|
||||
b1 = 3.0 * u * u * t
|
||||
b2 = 3.0 * u * t * t
|
||||
b3 = t * t * t
|
||||
self._current.append(
|
||||
(
|
||||
b0 * p0[0] + b1 * p1[0] + b2 * p2[0] + b3 * p3[0],
|
||||
b0 * p0[1] + b1 * p1[1] + b2 * p2[1] + b3 * p3[1],
|
||||
)
|
||||
)
|
||||
|
||||
def _closePath(self):
|
||||
if len(self._current) >= 3:
|
||||
self.contours.append(self._current)
|
||||
self._current = []
|
||||
|
||||
def _endPath(self):
|
||||
self._closePath()
|
||||
|
||||
return ContourPen
|
||||
|
||||
|
||||
def _scanline_mask(contours, width, height):
|
||||
"""Non-zero-winding scanline fill of `contours` into a `height` x `width` 0/1 grid.
|
||||
|
||||
The fill rule is non-zero rather than even-odd because TrueType orients a
|
||||
hole opposite its outer contour and a font is entitled to overlapping
|
||||
contours; even-odd would punch a hole out of any overlap.
|
||||
"""
|
||||
mask = [[0] * width for _ in range(height)]
|
||||
edges = []
|
||||
for contour in contours:
|
||||
count = len(contour)
|
||||
for i in range(count):
|
||||
x0, y0 = contour[i]
|
||||
x1, y1 = contour[(i + 1) % count]
|
||||
if y0 != y1:
|
||||
edges.append((x0, y0, x1, y1))
|
||||
for row in range(height):
|
||||
yc = row + 0.5
|
||||
crossings = []
|
||||
for x0, y0, x1, y1 in edges:
|
||||
if (y0 <= yc) != (y1 <= yc):
|
||||
t = (yc - y0) / (y1 - y0)
|
||||
crossings.append((x0 + t * (x1 - x0), 1 if y1 > y0 else -1))
|
||||
if not crossings:
|
||||
continue
|
||||
crossings.sort()
|
||||
winding = 0
|
||||
span_start = 0.0
|
||||
for x, direction in crossings:
|
||||
previous = winding
|
||||
winding += direction
|
||||
if previous == 0 and winding != 0:
|
||||
span_start = x
|
||||
elif previous != 0 and winding == 0:
|
||||
first = max(0, int(math.ceil(span_start - 0.5)))
|
||||
last = min(width, int(math.floor(x - 0.5)) + 1)
|
||||
target = mask[row]
|
||||
for column in range(first, last):
|
||||
target[column] = 1
|
||||
return mask
|
||||
|
||||
|
||||
def _coverage_cell(contours, cell_w, cell_h, supersample):
|
||||
"""Box-filter `contours` into `cell_h` rows of `cell_w` coverage bytes.
|
||||
|
||||
`contours` are in supersampled cell space, so each output texel averages the
|
||||
`supersample` x `supersample` sub-samples that land on it -- a plain coverage
|
||||
count, which is what makes the result greyscale.
|
||||
"""
|
||||
area = supersample * supersample
|
||||
mask = _scanline_mask(contours, cell_w * supersample, cell_h * supersample)
|
||||
rows = []
|
||||
for ty in range(cell_h):
|
||||
row = []
|
||||
for tx in range(cell_w):
|
||||
covered = 0
|
||||
for sy in range(supersample):
|
||||
sub = mask[ty * supersample + sy]
|
||||
base = tx * supersample
|
||||
for sx in range(supersample):
|
||||
covered += sub[base + sx]
|
||||
# Round-half-up in integers: deterministic, and independent of the
|
||||
# host's floating-point formatting.
|
||||
row.append((covered * 255 + area // 2) // area)
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def _cell_transform(em, units_per_em, ascent_px, supersample):
|
||||
scale = em / units_per_em
|
||||
|
||||
def xf(x_units, y_units):
|
||||
return (x_units * scale * supersample, (ascent_px - y_units * scale) * supersample)
|
||||
|
||||
return xf
|
||||
|
||||
|
||||
def _read_font(path):
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
if not os.path.isfile(path):
|
||||
raise SystemExit(_fail(EXIT_BAD_FONT, f"no such font file: {path}"))
|
||||
try:
|
||||
return TTFont(path, recalcBBoxes=False, recalcTimestamp=False)
|
||||
except Exception as exc: # fontTools raises a spread of parser errors
|
||||
raise SystemExit(_fail(EXIT_BAD_FONT, f"cannot parse {path}: {exc}"))
|
||||
|
||||
|
||||
def build(font_path, em, supersample):
|
||||
"""Rasterise the coverage set at `em` px; returns the geometry and cells."""
|
||||
font = _read_font(font_path)
|
||||
head = font["head"]
|
||||
os2 = font["OS/2"]
|
||||
units_per_em = head.unitsPerEm
|
||||
glyph_set = font.getGlyphSet()
|
||||
cmap = font.getBestCmap()
|
||||
hmtx = font["hmtx"]
|
||||
|
||||
name_record = font["name"]
|
||||
|
||||
def name(identifier, fallback):
|
||||
value = name_record.getDebugName(identifier)
|
||||
return value if value else fallback
|
||||
|
||||
scale = em / units_per_em
|
||||
ascent_units = os2.sTypoAscender
|
||||
descent_units = -os2.sTypoDescender
|
||||
ascent_px = ascent_units * scale
|
||||
|
||||
contours_class = _contour_pen_class()
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
|
||||
codepoints = list(ASCII) + list(SPECIALS)
|
||||
|
||||
# The cell: wide enough for the widest advance (the pen must never leave the
|
||||
# cell) and for the widest ink; tall enough for the face's ascent-to-descent
|
||||
# extent. Never trimmed to any one glyph's ink -- a fixed-cell grid is what
|
||||
# lets the pipeline carry one rect size per draw.
|
||||
widest_advance = 0
|
||||
widest_ink = 0.0
|
||||
for codepoint in codepoints:
|
||||
glyph_name = cmap.get(codepoint)
|
||||
if glyph_name is None:
|
||||
raise SystemExit(
|
||||
_fail(EXIT_BAD_FONT, f"{font_path} has no glyph for U+{codepoint:04X}")
|
||||
)
|
||||
advance, _bearing = hmtx[glyph_name]
|
||||
widest_advance = max(widest_advance, advance)
|
||||
bounds = BoundsPen(glyph_set)
|
||||
glyph_set[glyph_name].draw(bounds)
|
||||
if bounds.bounds is not None:
|
||||
widest_ink = max(widest_ink, bounds.bounds[2])
|
||||
|
||||
cell_w = int(math.ceil(max(widest_advance * scale, widest_ink * scale) - 1e-9))
|
||||
cell_h = int(math.ceil(ascent_px + descent_units * scale - 1e-9))
|
||||
if cell_w <= 0 or cell_h <= 0:
|
||||
raise SystemExit(_fail(EXIT_BAD_FONT, "the computed cell is empty"))
|
||||
|
||||
xf = _cell_transform(em, units_per_em, ascent_px, supersample)
|
||||
|
||||
cells = []
|
||||
advances = []
|
||||
for codepoint in codepoints:
|
||||
glyph_name = cmap[codepoint]
|
||||
pen = contours_class(glyph_set, xf)
|
||||
glyph_set[glyph_name].draw(pen)
|
||||
rows = _coverage_cell(pen.contours, cell_w, cell_h, supersample)
|
||||
ink = sum(sum(row) for row in rows)
|
||||
if codepoint != 0x20 and ink == 0:
|
||||
# A silent blank cell is the failure mode this whole exercise exists
|
||||
# to remove: refuse to ship it.
|
||||
raise SystemExit(
|
||||
_fail(
|
||||
EXIT_BAD_FONT,
|
||||
f"U+{codepoint:04X} ({glyph_name}) rasterised to no ink at "
|
||||
f"{em} px; the cell is too small or the glyph is missing",
|
||||
)
|
||||
)
|
||||
cells.append(rows)
|
||||
advances.append(hmtx[glyph_name][0])
|
||||
|
||||
fallback_pen = contours_class(glyph_set, xf)
|
||||
fallback_pen.contours = [
|
||||
[fallback_pen._xf(x, y) for x, y in FALLBACK_OUTER],
|
||||
[fallback_pen._xf(x, y) for x, y in FALLBACK_INNER],
|
||||
]
|
||||
fallback_rows = _coverage_cell(fallback_pen.contours, cell_w, cell_h, supersample)
|
||||
if sum(sum(row) for row in fallback_rows) == 0:
|
||||
raise SystemExit(_fail(EXIT_BAD_FONT, "the fallback box rasterised to no ink"))
|
||||
cells.append(fallback_rows)
|
||||
advances.append(widest_advance)
|
||||
|
||||
geometry = {
|
||||
"em": em,
|
||||
"supersample": supersample,
|
||||
"units_per_em": units_per_em,
|
||||
"scale": scale,
|
||||
"ascent_units": ascent_units,
|
||||
"descent_units": descent_units,
|
||||
"ascent_px": ascent_px,
|
||||
"cell_w": cell_w,
|
||||
"cell_h": cell_h,
|
||||
"glyph_count": len(codepoints) + 1,
|
||||
"fallback_slot": len(codepoints),
|
||||
"codepoints": codepoints,
|
||||
"advances": advances,
|
||||
"cells": cells,
|
||||
}
|
||||
provenance = {
|
||||
"font_file": os.path.basename(font_path),
|
||||
"font_name": name(4, "unknown"),
|
||||
"font_version": name(5, "unknown"),
|
||||
}
|
||||
font.close()
|
||||
return geometry, provenance
|
||||
|
||||
|
||||
def _codepoint_label(codepoint):
|
||||
if codepoint == FALLBACK_CODEPOINT:
|
||||
return "fallback box"
|
||||
return f"U+{codepoint:04X}"
|
||||
|
||||
|
||||
def emit(geometry, provenance):
|
||||
"""Render one `build()` result as the generated translation unit."""
|
||||
em = geometry["em"]
|
||||
supersample = geometry["supersample"]
|
||||
units_per_em = geometry["units_per_em"]
|
||||
cell_w = geometry["cell_w"]
|
||||
cell_h = geometry["cell_h"]
|
||||
glyph_count = geometry["glyph_count"]
|
||||
fallback_slot = geometry["fallback_slot"]
|
||||
atlas_w = cell_w * glyph_count
|
||||
atlas_h = cell_h
|
||||
codepoints = geometry["codepoints"]
|
||||
advances = geometry["advances"]
|
||||
slot_codepoints = list(codepoints) + [FALLBACK_CODEPOINT]
|
||||
|
||||
ascii_lo = ASCII[0]
|
||||
ascii_hi = ASCII[-1]
|
||||
|
||||
lines = []
|
||||
add = lines.append
|
||||
|
||||
add("// GENERATED FILE -- do not edit by hand.")
|
||||
add("//")
|
||||
add(f"// Regenerate with: {SCRIPT}")
|
||||
add(f"// The generator takes the source font, the em size and the supersample factor;")
|
||||
add(f"// its defaults bake the face this file holds. `{SCRIPT} --check`")
|
||||
add("// regenerates into memory and fails if this file has drifted from its input.")
|
||||
add("//")
|
||||
add("// Provenance")
|
||||
add("// source font : " + provenance["font_file"])
|
||||
add("// font name : " + provenance["font_name"])
|
||||
add("// font version : " + provenance["font_version"])
|
||||
add("// licence : SIL Open Font License 1.1 (licences/OFL-1.1.txt)")
|
||||
add(f"// units per em : {units_per_em}")
|
||||
add(f"// em size : {em} px")
|
||||
add(f"// supersample : {supersample}x ({supersample * supersample} samples per texel)")
|
||||
add(f"// coverage : U+{ascii_lo:04X}..U+{ascii_hi:04X}, "
|
||||
+ ", ".join(f"U+{cp:04X}" for cp in SPECIALS)
|
||||
+ ", plus a fallback box")
|
||||
add(f"// cell : {cell_w} x {cell_h} texels, one row of {glyph_count}")
|
||||
add(f"// atlas : {atlas_w} x {atlas_h} texels")
|
||||
add("//")
|
||||
add("// The cell is the face's maximum advance wide and its ascent-to-descent extent")
|
||||
add("// tall; every glyph is drawn into that cell with its bearing preserved, never")
|
||||
add("// trimmed to its ink, because the pipeline stages one rect *origin* per glyph and")
|
||||
add("// carries one rect *size* for the whole draw (see render::TextBackend::cell_uv).")
|
||||
add("// Coverage is a plain supersampled coverage count, so a stroke's edges are grey")
|
||||
add("// and not a 1-bit threshold.")
|
||||
add("")
|
||||
add("#include <plot/render/embedded_mono_font.hpp>")
|
||||
add("")
|
||||
add("#include <array>")
|
||||
add("#include <utility>")
|
||||
add("")
|
||||
add("namespace plot::render {")
|
||||
add("namespace {")
|
||||
add("")
|
||||
add("// Geometry, all of it derived from the bake above by the generator.")
|
||||
add(f"constexpr u32 kEmPx = {em}u;")
|
||||
add(f"constexpr u32 kSupersample = {supersample}u;")
|
||||
add(f"constexpr u32 kUnitsPerEm = {units_per_em}u;")
|
||||
add(f"constexpr u32 kCellW = {cell_w}u;")
|
||||
add(f"constexpr u32 kCellH = {cell_h}u;")
|
||||
add(f"constexpr u32 kGlyphCount = {glyph_count}u;")
|
||||
add(f"constexpr u32 kFallbackSlot = {fallback_slot}u;")
|
||||
add("constexpr u32 kAtlasW = kCellW * kGlyphCount;")
|
||||
add("constexpr u32 kAtlasH = kCellH;")
|
||||
add("constexpr u32 kAsciiFirst = " + f"{ascii_lo}u;")
|
||||
add("constexpr u32 kAsciiGlyphs = " + f"{len(ASCII)}u;")
|
||||
add("constexpr u32 kFallbackCodepoint = " + f"0x{FALLBACK_CODEPOINT:08X}u;")
|
||||
add("")
|
||||
add(f"constexpr u32 kAscentUnits = {geometry['ascent_units']}u;")
|
||||
add("// Font pixels per font unit. The one non-integer factor, and the only place a")
|
||||
add("// float enters: everything else here is an integer.")
|
||||
add("constexpr f32 kScale = static_cast<f32>(kEmPx) / static_cast<f32>(kUnitsPerEm);")
|
||||
add("constexpr f32 kAscentPx = static_cast<f32>(kAscentUnits) * kScale;")
|
||||
add("")
|
||||
add("// The public header repeats the em size, the supersample factor and the")
|
||||
add("// fallback marker so a caller can read them without opening this table.")
|
||||
add("// These asserts are what make that a pin and not a copy: re-baking at another")
|
||||
add("// em or supersample factor, or moving the fallback marker, fails to compile")
|
||||
add("// until the header agrees, so the two can never silently disagree.")
|
||||
add("static_assert(kEmPx == EmbeddedMonoFont::kEmPx,")
|
||||
add(" \"embedded_mono_font.hpp kEmPx disagrees with the bake\");")
|
||||
add("static_assert(kSupersample == EmbeddedMonoFont::kSupersample,")
|
||||
add(" \"embedded_mono_font.hpp kSupersample disagrees with the bake\");")
|
||||
add("static_assert(kFallbackCodepoint == EmbeddedMonoFont::kFallbackCodepoint,")
|
||||
add(" \"embedded_mono_font.hpp kFallbackCodepoint disagrees with the bake\");")
|
||||
add("")
|
||||
add("/// The codepoint each slot was baked for, in slot order; the fallback slot")
|
||||
add("/// carries `kFallbackCodepoint`, which is not a Unicode scalar.")
|
||||
add(f"constexpr u32 kCodepoints[kGlyphCount] = {{")
|
||||
for slot, codepoint in enumerate(codepoints):
|
||||
add(f" 0x{codepoint:04X}u, // [{slot:3d}] {_codepoint_label(codepoint)}")
|
||||
add(f" 0x{FALLBACK_CODEPOINT:08X}u, // [{fallback_slot:3d}] {_codepoint_label(FALLBACK_CODEPOINT)}")
|
||||
add("};")
|
||||
add("")
|
||||
add("/// The advance each slot contributes to the pen, in font units. The face is")
|
||||
add("/// monospaced, so these are all equal -- but the table is per glyph, because the")
|
||||
add("/// advance is the font's own `hmtx` value and not an assumption about the face.")
|
||||
add(f"constexpr u32 kAdvanceUnits[kGlyphCount] = {{")
|
||||
for slot, advance in enumerate(advances):
|
||||
codepoint = codepoints[slot] if slot < len(codepoints) else FALLBACK_CODEPOINT
|
||||
add(f" {advance}u, // [{slot:3d}] {_codepoint_label(codepoint)}")
|
||||
add("};")
|
||||
add("")
|
||||
add("/// The packed atlas: one row of cells, top row first, straight coverage, 0 for")
|
||||
add("/// background and 255 for full ink. GL_R8-ready, no dilation, no gamma.")
|
||||
add(f"constexpr u8 kAtlas[kAtlasW * kAtlasH] = {{")
|
||||
for row in range(atlas_h):
|
||||
for slot in range(glyph_count):
|
||||
values = geometry["cells"][slot][row]
|
||||
body = ", ".join(f"0x{value:02X}" for value in values)
|
||||
label = _codepoint_label(slot_codepoints[slot])
|
||||
add(f" {body}, // [{slot:3d}] {label} row {row:2d}")
|
||||
add("};")
|
||||
add("")
|
||||
add("/// One slot's glyph: the cell it occupies in the atlas, drawn from the cell's")
|
||||
add("/// top-left (the pen) with no extra bearing -- the bearing is baked into the")
|
||||
add("/// cell's ink -- and the advance the pen moves by.")
|
||||
add("constexpr Glyph make_glyph(u32 slot) noexcept {")
|
||||
add(" const f32 u0 = static_cast<f32>(slot * kCellW) / static_cast<f32>(kAtlasW);")
|
||||
add(" const f32 du = static_cast<f32>(kCellW) / static_cast<f32>(kAtlasW);")
|
||||
add(" const f32 dv = static_cast<f32>(kCellH) / static_cast<f32>(kAtlasH);")
|
||||
add(" return Glyph{u0, 0.0f, u0 + du, dv, 0.0f, 0.0f,")
|
||||
add(" static_cast<f32>(kAdvanceUnits[slot]) * kScale};")
|
||||
add("}")
|
||||
add("")
|
||||
add("template <u32... Slots>")
|
||||
add("constexpr std::array<Glyph, sizeof...(Slots)> make_glyphs(")
|
||||
add(" std::integer_sequence<u32, Slots...>) noexcept {")
|
||||
add(" return {make_glyph(Slots)...};")
|
||||
add("}")
|
||||
add("")
|
||||
add("constexpr std::array<Glyph, kGlyphCount> kGlyphs =")
|
||||
add(" make_glyphs(std::make_integer_sequence<u32, kGlyphCount>{});")
|
||||
add("")
|
||||
add("} // namespace")
|
||||
add("")
|
||||
add("u32 EmbeddedMonoFont::glyph_index(u32 codepoint) noexcept {")
|
||||
add(" if (codepoint >= kAsciiFirst && codepoint < kAsciiFirst + kAsciiGlyphs) {")
|
||||
add(" return codepoint - kAsciiFirst;")
|
||||
add(" }")
|
||||
add(" switch (codepoint) {")
|
||||
for offset, codepoint in enumerate(SPECIALS):
|
||||
add(f" case 0x{codepoint:04X}u:")
|
||||
add(f" return kAsciiGlyphs + {offset}u;")
|
||||
add(" default:")
|
||||
add(" return kFallbackSlot;")
|
||||
add(" }")
|
||||
add("}")
|
||||
add("")
|
||||
add("f32 EmbeddedMonoFont::advance_codepoint(u32 codepoint) noexcept {")
|
||||
add(" return static_cast<f32>(kAdvanceUnits[glyph_index(codepoint)]) * kScale;")
|
||||
add("}")
|
||||
add("")
|
||||
add("f32 EmbeddedMonoFont::measure(const char* utf8) const {")
|
||||
add(" if (utf8 == nullptr) {")
|
||||
add(" return 0.0f;")
|
||||
add(" }")
|
||||
add(" f32 width = 0.0f;")
|
||||
add(" for (const char* p = utf8; *p != '\\0';) {")
|
||||
add(" width += advance_codepoint(next_codepoint(p));")
|
||||
add(" }")
|
||||
add(" return width;")
|
||||
add("}")
|
||||
add("")
|
||||
add("f32 EmbeddedMonoFont::height() const { return static_cast<f32>(kCellH); }")
|
||||
add("")
|
||||
add("f32 EmbeddedMonoFont::ascent() const { return kAscentPx; }")
|
||||
add("")
|
||||
add("f32 EmbeddedMonoFont::em_px() const { return static_cast<f32>(kEmPx); }")
|
||||
add("")
|
||||
add("const Glyph* EmbeddedMonoFont::glyph(u32 codepoint) const {")
|
||||
add(" return &kGlyphs[glyph_index(codepoint)];")
|
||||
add("}")
|
||||
add("")
|
||||
add("Vec2 EmbeddedMonoFont::cell_uv() const {")
|
||||
add(" return Vec2{static_cast<f32>(kCellW) / static_cast<f32>(kAtlasW),")
|
||||
add(" static_cast<f32>(kCellH) / static_cast<f32>(kAtlasH)};")
|
||||
add("}")
|
||||
add("")
|
||||
add("const u8* EmbeddedMonoFont::atlas_pixels() const { return kAtlas; }")
|
||||
add("")
|
||||
add("u32 EmbeddedMonoFont::atlas_width() const { return kAtlasW; }")
|
||||
add("")
|
||||
add("u32 EmbeddedMonoFont::atlas_height() const { return kAtlasH; }")
|
||||
add("")
|
||||
add("u32 EmbeddedMonoFont::revision() const { return 0u; }")
|
||||
add("")
|
||||
add("u32 EmbeddedMonoFont::glyph_count() noexcept { return kGlyphCount; }")
|
||||
add("")
|
||||
add("u32 EmbeddedMonoFont::codepoint_of_slot(u32 slot) noexcept {")
|
||||
add(" return slot < kGlyphCount ? kCodepoints[slot] : kFallbackCodepoint;")
|
||||
add("}")
|
||||
add("")
|
||||
add("u32 EmbeddedMonoFont::cell_width() noexcept { return kCellW; }")
|
||||
add("")
|
||||
add("u32 EmbeddedMonoFont::cell_height() noexcept { return kCellH; }")
|
||||
add("")
|
||||
add("Vec2 EmbeddedMonoFont::atlas_origin(u32 slot) noexcept {")
|
||||
add(" const u32 s = slot < kGlyphCount ? slot : kFallbackSlot;")
|
||||
add(" return Vec2{static_cast<f32>(s * kCellW), 0.0f};")
|
||||
add("}")
|
||||
add("")
|
||||
add("const u8* EmbeddedMonoFont::atlas_image() noexcept { return kAtlas; }")
|
||||
add("")
|
||||
add("} // namespace plot::render")
|
||||
add("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate(font_path, em, supersample):
|
||||
geometry, provenance = build(font_path, em, supersample)
|
||||
return emit(geometry, provenance), geometry
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--font", default=DEFAULT_FONT, help="source TrueType/OpenType file")
|
||||
parser.add_argument("--em", type=int, default=DEFAULT_EM, help="em size in pixels")
|
||||
parser.add_argument(
|
||||
"--supersample", type=int, default=DEFAULT_SUPERSAMPLE, help="coverage samples per texel"
|
||||
)
|
||||
parser.add_argument("--out", default=DEFAULT_OUT, help="translation unit to write")
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="regenerate and fail if the committed file differs, instead of writing",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.em <= 0 or args.supersample <= 0:
|
||||
return _fail(EXIT_BAD_FONT, "em size and supersample factor must be positive")
|
||||
|
||||
_require_fonttools()
|
||||
|
||||
if args.check:
|
||||
out = Path(args.out)
|
||||
committed = out.read_bytes() if out.is_file() else None
|
||||
fresh, _ = generate(args.font, args.em, args.supersample)
|
||||
if committed is None:
|
||||
return _fail(EXIT_MISMATCH, f"{out} does not exist; run {SCRIPT} to generate it")
|
||||
if committed != fresh.encode():
|
||||
return _fail(
|
||||
EXIT_MISMATCH,
|
||||
f"{out} differs from a fresh bake; regenerate with {SCRIPT} and commit the result",
|
||||
)
|
||||
print(f"{SCRIPT}: {out} matches a fresh bake", file=sys.stderr)
|
||||
return EXIT_OK
|
||||
|
||||
text, geometry = generate(args.font, args.em, args.supersample)
|
||||
Path(args.out).write_bytes(text.encode())
|
||||
ink = sum(sum(row) for cell in geometry["cells"] for row in cell)
|
||||
print(
|
||||
f"{SCRIPT}: {args.out}: {geometry['glyph_count']} glyphs, "
|
||||
f"cell {geometry['cell_w']}x{geometry['cell_h']}, "
|
||||
f"atlas {geometry['cell_w'] * geometry['glyph_count']}x{geometry['cell_h']}, "
|
||||
f"{args.em} px em, {args.supersample}x, {ink} ink units",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user