.font asset type
A font is a general CPU resource usable by any text surface. The
onRegister hook calls the engine font.register(name, bytes) primitive,
which installs the parsed face into both the 2D/3D text system (text.*)
and the egui UI text system, under the family name the font file itself
reports. A registration is what puts the face in the database; whether
fontFamily = "<name>" selects it is the separate question of whether the
shaper matches that name, and font.reconcile() answers it per family:
for _, f in ipairs(font.reconcile()) do
if not f.selectable then
print(f.family .. " is registered but shapes as " .. f.shapedWith)
end
end
text.face(handle) gives the same answer for one label. A family the shaper's
database holds is selectable under its own weights and for the glyphs its faces
cover: a row that reads familyCoveredNoGlyph names a family the shaper picks
whose faces have no glyph for the text asked of it. topics/text covers reading
a label that came out in the wrong face.
Fonts are CPU resources (not GPU), so the primitive lives in the font.*
namespace, the CPU analogue of renderer.texture.create.
Layout
<name>.font/
data.zfnt the baked payload `asset.create("font", ...)` writes
face.json optional: family group, role, aliases, on-demand entries
Editing a font's files
A write to a file the registration reads (face.json, data.zfnt, or a raw
font file in the folder) installs the font again from what the folder now
holds. The names the new face.json gives it (its group and each alias) select
it on every text surface from then on, and a name the earlier descriptor gave it
and the new one does not stops selecting it; a changed group moves the face to
that group. Text laid out under a name the edit decides is laid out again, and a
Text3D label draws it again on the next frame. For an on-demand face the
same holds for the font and coverage files its entries name: the entries are
declared again from their coverage files, and an entry already installed is
registered again from its font file with the names the descriptor now gives.
On-demand faces
A face whose file is large and whose script most sessions never draw is
declared by the codepoints it covers instead of being read at boot. Its
face.json lists the font files of the folder under onDemand, each beside a
coverage file:
NotoSansCJK.font/
face.json { group, role, aliases, license, onDemand = [ { name, file, coverage }, ... ] }
kana.otf a font file (TTF / OTF)
kana.coverage the codepoints it maps: "3000-30ff,ff01-ff5e,..."
...
OFL.txt the license the files are distributed under
onRegister reads the coverage files and calls font.declare(name, coverage, { file, family, role, aliases }) for each entry; no font file is read. The first
time a label or a text.* object meets a character an entry covers and no
installed face draws, the engine requests that entry: its file is read without
blocking the frame and registered with font.register, and from then on every
text surface draws the character from it. font.onDemand() reports each entry
as declared, requested, installed or failed, and font.prefetch(text)
requests the entries a text needs before it is drawn.
font.coverage(bytes) writes a coverage file from a font file's character map.
Splitting one large face into several entries (by script, and by how common a
character is) means a session reads only the part its text reaches: the
built-in NotoSansCJK.font is cut that way by scripts/fonts/build_cjk_fallback.py,
and a page of Japanese reads its kana and common-ideograph entries alone.
Every registered face, on-demand or not, is a per-glyph fallback for every family the UI holds: a character the selected family has no glyph for is drawn from the first face that has one.
In the Inspector
Selecting a font opens its Font section: the family the face registers under, its role and aliases from face.json, whether the text system has the family registered and, when it holds it, how many faces and which weights. Below the facts, a sample line is drawn in the family at 14, 20 and 32 px.