fix(engine): regenerate default theme SVGs to Terminal aesthetic
Step 4's PNG regeneration left the cards looking unchanged at
runtime because the PNGs at assets/cards/ are only the *fallback*
art — production renders the bundled-default theme's SVGs, which
get include_bytes!()-embedded into the binary by
solitaire_engine::assets::sources and applied to CardImageSet at
startup by theme::plugin::apply_theme_to_card_image_set. Those
SVGs were still the legacy vector-playing-cards art.
Extends card_face_generator to write SVGs into both runtime
paths in lockstep:
1. assets/cards/{faces,backs}/*.png — fallback art (unchanged
from step 4).
2. solitaire_engine/assets/themes/default/*.svg — what production
actually renders. 52 face SVGs + 1 back SVG, generated from
the same face_svg / back_svg builders as the PNGs so the two
paths can never visually diverge.
Adds two helper functions to card_face_svg:
- theme_suit_token (clubs/diamonds/hearts/spades — lowercase
full word, matching CardKey::manifest_name)
- theme_rank_token (ace/2..10/jack/queen/king — same)
The theme back uses BACK_ACCENTS[0] (canonical Terminal cyan).
The other four accents only live as PNG fallbacks because the
theme system carries one back per theme.
Net SVG diff: -14884 / +940 lines — the legacy vector-playing-
cards SVGs were ~300 lines each of Inkscape-authored paths;
the Terminal SVGs are ~10 lines of programmatic output.
Workspace clippy + cargo test --workspace clean. Pin test
unaffected (the SVG builders themselves did not change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
//! Card-face generator — writes 52 Terminal-aesthetic face PNGs +
|
||||
//! 5 back PNGs into `assets/cards/`.
|
||||
//! Card-face generator — writes Terminal-aesthetic artwork into both
|
||||
//! rendering paths the engine consults at runtime:
|
||||
//!
|
||||
//! 1. **Asset PNGs at `assets/cards/`** — 52 face + 5 back PNGs loaded
|
||||
//! by `card_plugin::load_card_images` as the *fallback* art.
|
||||
//! 2. **Default-theme SVGs at `solitaire_engine/assets/themes/default/`**
|
||||
//! — 52 face + 1 back SVGs that get `include_bytes!()`-embedded into
|
||||
//! the binary by `solitaire_engine::assets::sources` and applied to
|
||||
//! `CardImageSet` at startup by `theme::plugin::apply_theme_to_card_image_set`.
|
||||
//! These *override* the asset PNGs in production; the PNGs only show
|
||||
//! if the active theme fails to provide a face.
|
||||
//!
|
||||
//! Both paths share the same SVG builders in
|
||||
//! `solitaire_engine::assets::card_face_svg`, so the artwork stays
|
||||
//! identical at the source level — running this generator keeps both
|
||||
//! paths in lockstep.
|
||||
//!
|
||||
//! Run with:
|
||||
//!
|
||||
@@ -7,63 +21,90 @@
|
||||
//! cargo run --example card_face_generator --release
|
||||
//! ```
|
||||
//!
|
||||
//! This is **step 2** of the lockstep migration outlined in
|
||||
//! `docs/ui-mockups/card-face-migration.md`. Running it overwrites
|
||||
//! the legacy PNG artwork in-place; the resulting bytes are what
|
||||
//! step 4 commits alongside the `card_plugin` constant migration.
|
||||
//!
|
||||
//! The SVG builders live in
|
||||
//! `solitaire_engine::assets::card_face_svg` so the integration
|
||||
//! test at `tests/card_face_svg_pin.rs` can pin their output
|
||||
//! against `usvg`/`resvg` rendering drift.
|
||||
//! Step 2 of the lockstep migration outlined in
|
||||
//! `docs/ui-mockups/card-face-migration.md`. Running it overwrites the
|
||||
//! legacy artwork in-place; the resulting bytes are what step 4 commits
|
||||
//! alongside the `card_plugin` constant migration.
|
||||
|
||||
use solitaire_engine::assets::card_face_svg::{
|
||||
back_svg, face_svg, rank_filename, suit_filename, ALL_RANKS, ALL_SUITS, BACK_ACCENTS, TARGET,
|
||||
back_svg, face_svg, rank_filename, suit_filename, theme_rank_token, theme_suit_token,
|
||||
ALL_RANKS, ALL_SUITS, BACK_ACCENTS, TARGET,
|
||||
};
|
||||
use solitaire_engine::assets::rasterize_svg;
|
||||
use std::path::PathBuf;
|
||||
use tiny_skia::{IntSize, Pixmap};
|
||||
|
||||
fn main() {
|
||||
let cards_dir = workspace_assets_dir().join("cards");
|
||||
let workspace_assets = workspace_assets_dir();
|
||||
let cards_dir = workspace_assets.join("cards");
|
||||
let faces_dir = cards_dir.join("faces");
|
||||
let backs_dir = cards_dir.join("backs");
|
||||
std::fs::create_dir_all(&faces_dir).expect("create faces dir");
|
||||
std::fs::create_dir_all(&backs_dir).expect("create backs dir");
|
||||
|
||||
let mut written = 0usize;
|
||||
// The default theme lives inside the engine crate (so its SVGs can
|
||||
// be `include_bytes!()`-embedded relative to the `assets/sources.rs`
|
||||
// file path). Workspace-level `assets/cards/` is the fallback path;
|
||||
// engine-level `assets/themes/default/` is what production renders.
|
||||
let theme_dir = engine_default_theme_dir();
|
||||
std::fs::create_dir_all(&theme_dir).expect("create default-theme dir");
|
||||
|
||||
let mut png_written = 0usize;
|
||||
let mut svg_written = 0usize;
|
||||
|
||||
for suit in ALL_SUITS {
|
||||
for rank in ALL_RANKS {
|
||||
let svg = face_svg(rank, suit);
|
||||
let pixmap = rasterize_to_pixmap(&svg);
|
||||
let path = faces_dir.join(format!(
|
||||
|
||||
// Path 1 — fallback PNG.
|
||||
let png_path = faces_dir.join(format!(
|
||||
"{}{}.png",
|
||||
rank_filename(rank),
|
||||
suit_filename(suit)
|
||||
));
|
||||
pixmap
|
||||
.save_png(&path)
|
||||
.unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
|
||||
written += 1;
|
||||
rasterize_to_pixmap(&svg)
|
||||
.save_png(&png_path)
|
||||
.unwrap_or_else(|e| panic!("write {}: {e}", png_path.display()));
|
||||
png_written += 1;
|
||||
|
||||
// Path 2 — bundled-default-theme SVG. Same SVG bytes; the
|
||||
// theme system rasterises them at runtime.
|
||||
let svg_path = theme_dir.join(format!(
|
||||
"{}_{}.svg",
|
||||
theme_suit_token(suit),
|
||||
theme_rank_token(rank),
|
||||
));
|
||||
std::fs::write(&svg_path, &svg)
|
||||
.unwrap_or_else(|e| panic!("write {}: {e}", svg_path.display()));
|
||||
svg_written += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback backs — 5 PNGs, one per `Settings::selected_card_back`.
|
||||
for (idx, accent) in BACK_ACCENTS.iter().enumerate() {
|
||||
let svg = back_svg(accent);
|
||||
let pixmap = rasterize_to_pixmap(&svg);
|
||||
let path = backs_dir.join(format!("back_{idx}.png"));
|
||||
pixmap
|
||||
.save_png(&path)
|
||||
.unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
|
||||
written += 1;
|
||||
let png_path = backs_dir.join(format!("back_{idx}.png"));
|
||||
rasterize_to_pixmap(&svg)
|
||||
.save_png(&png_path)
|
||||
.unwrap_or_else(|e| panic!("write {}: {e}", png_path.display()));
|
||||
png_written += 1;
|
||||
}
|
||||
|
||||
// Theme back — single SVG. Use the canonical Terminal accent
|
||||
// (`BACK_ACCENTS[0]` cyan) — the theme system only carries one back
|
||||
// per theme, and the canonical Terminal back is the design-system
|
||||
// default. The other four accents only live as PNG fallbacks.
|
||||
let theme_back_path = theme_dir.join("back.svg");
|
||||
std::fs::write(&theme_back_path, back_svg(BACK_ACCENTS[0]))
|
||||
.unwrap_or_else(|e| panic!("write {}: {e}", theme_back_path.display()));
|
||||
svg_written += 1;
|
||||
|
||||
println!(
|
||||
"Wrote {written} PNGs ({}×{} RGBA8) to {}",
|
||||
"Wrote {png_written} PNGs ({}×{} RGBA8) to {} and {svg_written} SVGs to {}",
|
||||
TARGET.x,
|
||||
TARGET.y,
|
||||
cards_dir.display(),
|
||||
theme_dir.display(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,3 +129,13 @@ fn workspace_assets_dir() -> PathBuf {
|
||||
.expect("solitaire_engine crate has a workspace-root parent")
|
||||
.join("assets")
|
||||
}
|
||||
|
||||
/// Resolves `solitaire_engine/assets/themes/default/` relative to the
|
||||
/// example crate. Matches `DEFAULT_THEME_MANIFEST_PATH` in
|
||||
/// `solitaire_engine::assets::sources`.
|
||||
fn engine_default_theme_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets")
|
||||
.join("themes")
|
||||
.join("default")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user