fix(engine): bundle fonts only and drop system-font fallback

Code-review feedback: the SVG rasteriser mixed three font-resolution
layers (load_system_fonts + bundled FiraMono + lenient resolver
appending CSS generics), which made card text rendering depend on
which fonts the host machine happened to have. The Bevy UI face
loaded separately at runtime via AssetServer. Picking option (a)
from the review and applying it consistently: bundle FiraMono via
include_bytes!() in BOTH layers, no system fallback anywhere.

solitaire_engine/src/font_plugin.rs now embeds main.ttf at compile
time and registers it with Assets<Font>. A parse failure aborts
with "bundled FiraMono failed to parse — binary is corrupt"; the
MinimalPlugins early-return stays as a "this plugin doesn't apply
in headless tests" check (consumers query Option<Res<FontResource>>
and degrade cleanly), not a production fallback.

solitaire_engine/src/assets/svg_loader.rs drops load_system_fonts
entirely, drops the lenient_font_resolver, and drops the five
set_*_family pins. The new bundled_font_resolver ignores the SVG's
font-family request and always returns the single bundled face —
the bundled card SVGs reference Arial / Bitstream Vera Sans by name
and we deliberately don't ship those, so routing every query to
FiraMono keeps rasterisation deterministic. shared_fontdb asserts
the embedded bytes parsed.

The two layers now embed the same path
(assets/fonts/main.ttf) independently, so they can't drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-02 18:33:54 +00:00
parent 61d891fb76
commit 17f9b518f1
2 changed files with 55 additions and 99 deletions
+23 -12
View File
@@ -1,14 +1,23 @@
// Register FontPlugin in solitaire_engine/src/lib.rs before use.
//! Loads FiraMono-Medium via the Bevy `AssetServer` and exposes it via [`FontResource`].
//! Embeds FiraMono-Medium into the binary and exposes it via [`FontResource`].
//!
//! Bundling rather than runtime-loading guarantees the canonical UI face is
//! always available regardless of install or platform. The bytes are
//! validated at startup; a parse failure aborts the program with a clear
//! error because it means the binary is corrupt.
use bevy::prelude::*;
/// Holds the project-wide [`Handle<Font>`] loaded at startup.
/// FiraMono-Medium bytes embedded at compile time. Single source of truth for
/// the project's UI face — `solitaire_engine::assets::svg_loader` embeds the
/// same path independently for SVG rasterisation so the two layers can't
/// drift.
const BUNDLED_FONT_BYTES: &[u8] = include_bytes!("../../assets/fonts/main.ttf");
/// Holds the project-wide [`Handle<Font>`] registered at startup.
#[derive(Resource)]
pub struct FontResource(pub Handle<Font>);
/// Loads FiraMono-Medium at startup and inserts [`FontResource`].
/// Registers the bundled FiraMono with [`Assets<Font>`] at startup.
pub struct FontPlugin;
impl Plugin for FontPlugin {
@@ -17,11 +26,13 @@ impl Plugin for FontPlugin {
}
}
fn load_font(asset_server: Option<Res<AssetServer>>, mut commands: Commands) {
let Some(asset_server) = asset_server else {
// AssetServer absent (e.g. MinimalPlugins in tests) — insert default.
commands.insert_resource(FontResource(Handle::default()));
return;
};
commands.insert_resource(FontResource(asset_server.load("fonts/main.ttf")));
fn load_font(fonts: Option<ResMut<Assets<Font>>>, mut commands: Commands) {
// Headless test fixtures use MinimalPlugins (no AssetPlugin → no
// Assets<Font>). FontPlugin in that context is a no-op — consumers
// already query `Option<Res<FontResource>>` and degrade cleanly.
let Some(mut fonts) = fonts else { return };
let font = Font::try_from_bytes(BUNDLED_FONT_BYTES.to_vec())
.expect("bundled FiraMono failed to parse — binary is corrupt");
let handle = fonts.add(font);
commands.insert_resource(FontResource(handle));
}