Files
Ferrous-Solitaire/solitaire_engine/examples/card_face_generator.rs
T
funman300 6e407a3ea7
Build and Deploy / build-and-push (push) Successful in 3m54s
fix(engine,server): safe area clamp, analytics batch, achievement save order, daily rollover, replay validation, leaderboard opt-in (#56, #60, #61, #62, #66, #68)
- #66: Clamp safe-area insets to 25% of window height with warn!() on excess
- #68: Move fire_flush outside per-event loop in analytics (batch flush once)
- #56: Persist progress before marking reward_granted to prevent XP loss on crash
- #60: Add DateRolloverTimer + check_date_rollover system for midnight seed refresh
- #62: Add validate_header() in replay upload with mode/draw_mode allowlists
- #61: Restore two-query leaderboard opt-in check (SELECT then UPDATE); original
       queries already in .sqlx cache; EXISTS variant would require sqlx prepare

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 13:07:22 -07:00

142 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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:
//!
//! ```sh
//! cargo run --example card_face_generator --release
//! ```
//!
//! 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::{
ALL_RANKS, ALL_SUITS, BACK_ACCENTS, TARGET, back_svg, face_svg, rank_filename, suit_filename,
theme_rank_token, theme_suit_token,
};
use solitaire_engine::assets::rasterize_svg;
use std::path::PathBuf;
use tiny_skia::{IntSize, Pixmap};
fn main() {
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");
// 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);
// Path 1 — fallback PNG.
let png_path = faces_dir.join(format!(
"{}{}.png",
rank_filename(rank),
suit_filename(suit)
));
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 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 {png_written} PNGs ({}×{} RGBA8) to {} and {svg_written} SVGs to {}",
TARGET.x,
TARGET.y,
cards_dir.display(),
theme_dir.display(),
);
}
fn rasterize_to_pixmap(svg: &str) -> Pixmap {
let image = rasterize_svg(svg.as_bytes(), TARGET).expect("rasterise card SVG");
let bytes = image.data.expect("rasterised image carries pixel data");
debug_assert_eq!(
bytes.len(),
(TARGET.x * TARGET.y * 4) as usize,
"rasterised buffer must match width × height × 4 RGBA bytes",
);
let size = IntSize::from_wh(TARGET.x, TARGET.y).expect("non-zero target size");
Pixmap::from_vec(bytes, size).expect("RGBA buffer forms a valid Pixmap")
}
/// Resolves the workspace-root `assets/` directory relative to the
/// running example crate (`solitaire_engine/`). `CARGO_MANIFEST_DIR`
/// is the engine crate; its parent is the workspace root.
fn workspace_assets_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.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")
}