Files
Ferrous-Solitaire/solitaire_engine/examples/card_face_generator.rs
T
funman300 48b28d29f8 test(engine): pin card-face SVG output against rasteriser drift
Step 3 of the migration plan in docs/ui-mockups/card-face-migration.md.

Extracts face_svg / back_svg + palette constants from the
card_face_generator example into a new
solitaire_engine::assets::card_face_svg module so an integration
test can call them. The example becomes a thin wrapper.

The new tests/card_face_svg_pin.rs hashes the raw RGBA8 pixel
bytes from rasterising every face × suit + every back accent and
compares each FNV-1a fingerprint against an embedded constant.
Catches silent rendering drift if usvg / resvg / tiny_skia / the
bundled FiraMono ever change in a way that perturbs pixels.

Hashing is FNV-1a inline (~5 lines) rather than adding sha2 or
blake3 — cryptographic strength isn't load-bearing here, just
stable byte fingerprints.

When the SVG builders intentionally change, empty EXPECTED to
`&[]` and re-run the test once; it panics with the new hashes
formatted as Rust source ready to paste back in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:21:00 -07:00

91 lines
3.1 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 52 Terminal-aesthetic face PNGs +
//! 5 back PNGs into `assets/cards/`.
//!
//! Run with:
//!
//! ```sh
//! 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.
use solitaire_engine::assets::card_face_svg::{
back_svg, face_svg, rank_filename, suit_filename, 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 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;
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!(
"{}{}.png",
rank_filename(rank),
suit_filename(suit)
));
pixmap
.save_png(&path)
.unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
written += 1;
}
}
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;
}
println!(
"Wrote {written} PNGs ({}×{} RGBA8) to {}",
TARGET.x,
TARGET.y,
cards_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")
}