From b8fb3fbd6e5beb367cecb79492a955b2f5192e42 Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 1 May 2026 05:05:30 +0000 Subject: [PATCH] =?UTF-8?q?feat(engine):=20SVG=20=E2=86=92=20Image=20asset?= =?UTF-8?q?=20loader=20(Card=20theme=20phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the runtime SVG rasterisation pipeline that the card-theme system (CARD_PLAN.md) is built on. Bevy 0.18 has no native SVG support; this loader bridges usvg (parser) + resvg (renderer) + tiny-skia (CPU pixmap) so the rest of the engine consumes themes as plain Handle. Rasterisation happens once per (asset, settings) pair at load time — Bevy's asset cache absorbs the cost. solitaire_engine/src/assets/ mod.rs — module entrypoint svg_loader.rs — SvgLoader (AssetLoader for .svg → Image) SvgLoaderSettings { target_size: UVec2 } default 512×768 SvgLoaderError (Io / Parse / PixmapAlloc) via thiserror rasterize_svg() helper exposed for non-asset-graph callers (the future zip-importer validation step) The rasteriser scales-to-fit while preserving aspect ratio, centring the SVG inside the target box so a non-2:3 source doesn't pin to the top-left corner. 7 new unit tests — default + custom target size, zero-dimension reject, malformed-input reject, RGBA byte-count, extension advertisement, and a compile-time guard that SvgLoaderSettings still satisfies the AssetLoader::Settings trait bounds. Workspace deps added: usvg 0.47, resvg 0.47, tiny-skia 0.12 (latest minor versions; CARD_PLAN.md called out the placeholder numbers needed verification). cargo build / cargo clippy --workspace --all-targets -- -D warnings / cargo test --workspace all green (913 passed, 0 failed, 9 ignored — +7 from the new loader tests). --- Cargo.lock | 265 ++++++++++++++++++++-- Cargo.toml | 8 + solitaire_engine/Cargo.toml | 5 + solitaire_engine/src/assets/mod.rs | 10 + solitaire_engine/src/assets/svg_loader.rs | 207 +++++++++++++++++ solitaire_engine/src/lib.rs | 1 + 6 files changed, 482 insertions(+), 14 deletions(-) create mode 100644 solitaire_engine/src/assets/mod.rs create mode 100644 solitaire_engine/src/assets/svg_loader.rs diff --git a/Cargo.lock b/Cargo.lock index 4aea29d..4579882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2429,6 +2429,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.5" @@ -2950,6 +2956,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "datasketches" version = "0.2.0" @@ -3479,6 +3491,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "flume" version = "0.11.1" @@ -3532,7 +3550,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" dependencies = [ - "roxmltree", + "roxmltree 0.20.0", ] [[package]] @@ -3851,6 +3869,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gilrs" version = "0.11.1" @@ -4529,6 +4557,22 @@ dependencies = [ "png 0.18.1", ] +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + [[package]] name = "indexmap" version = "2.14.0" @@ -4856,6 +4900,17 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "kurbo" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -6011,15 +6066,6 @@ dependencies = [ "libredox", ] -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-float" version = "5.3.0" @@ -6165,6 +6211,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.11" @@ -6428,6 +6480,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.39.2" @@ -6798,6 +6856,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "resvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be183ad6a216aa96f33e4c8033b0988b8b3ea6fd2359d19af5bac4643fd8e81" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia 0.12.0", + "usvg", + "zune-jpeg", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -6808,6 +6883,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.14" @@ -6862,6 +6946,15 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + [[package]] name = "rpassword" version = "7.5.0" @@ -7068,6 +7161,24 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ruzstd" version = "0.8.2" @@ -7123,7 +7234,7 @@ dependencies = [ "log", "memmap2", "smithay-client-toolkit", - "tiny-skia", + "tiny-skia 0.11.4", ] [[package]] @@ -7382,6 +7493,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + [[package]] name = "simsimd" version = "6.5.16" @@ -7558,10 +7678,15 @@ dependencies = [ "bevy", "chrono", "kira", + "resvg", + "serde", "solitaire_core", "solitaire_data", "solitaire_sync", + "thiserror 2.0.18", + "tiny-skia 0.12.0", "tokio", + "usvg", "uuid", ] @@ -7860,6 +7985,9 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] [[package]] name = "stringprep" @@ -7912,6 +8040,16 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + [[package]] name = "swash" version = "0.2.7" @@ -8226,7 +8364,7 @@ checksum = "dfadb8526b6da90704feb293b0701a6aae62ea14983143344be2dc5ce30f1d82" dependencies = [ "fnv", "nom", - "ordered-float 5.3.0", + "ordered-float", "serde", "serde_json", ] @@ -8383,7 +8521,22 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "tiny-skia-path", + "tiny-skia-path 0.11.4", +] + +[[package]] +name = "tiny-skia" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.18.1", + "tiny-skia-path 0.12.0", ] [[package]] @@ -8397,6 +8550,17 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -9015,6 +9179,18 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -9054,6 +9230,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.14" @@ -9094,6 +9276,34 @@ dependencies = [ "serde", ] +[[package]] +name = "usvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree 0.21.1", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path 0.12.0", + "ttf-parser", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "utf8-ranges" version = "1.0.5" @@ -9453,6 +9663,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "wgpu" version = "27.0.1" @@ -9571,7 +9787,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "objc", "once_cell", - "ordered-float 4.6.0", + "ordered-float", "parking_lot", "portable-atomic", "portable-atomic-util", @@ -10500,6 +10716,12 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yazi" version = "0.2.1" @@ -10735,6 +10957,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.10.1" diff --git a/Cargo.toml b/Cargo.toml index 2ce97fd..e600d4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,14 @@ solitaire_engine = { path = "solitaire_engine" } bevy = "0.18" kira = "0.12" +# SVG rasterisation pipeline for the runtime card-theme system. +# usvg parses + simplifies; resvg renders to a tiny-skia Pixmap; +# tiny-skia provides the CPU rasteriser. All three are maintained +# together by the resvg-rs project and version in lockstep. +usvg = "0.47" +resvg = "0.47" +tiny-skia = "0.12" + axum = "0.8" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } jsonwebtoken = { version = "10", default-features = false, features = ["rust_crypto"] } diff --git a/solitaire_engine/Cargo.toml b/solitaire_engine/Cargo.toml index ea56ae5..de3adbb 100644 --- a/solitaire_engine/Cargo.toml +++ b/solitaire_engine/Cargo.toml @@ -13,6 +13,11 @@ solitaire_sync = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } tokio = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +usvg = { workspace = true } +resvg = { workspace = true } +tiny-skia = { workspace = true } [dev-dependencies] async-trait = { workspace = true } diff --git a/solitaire_engine/src/assets/mod.rs b/solitaire_engine/src/assets/mod.rs new file mode 100644 index 0000000..a21f056 --- /dev/null +++ b/solitaire_engine/src/assets/mod.rs @@ -0,0 +1,10 @@ +//! Asset-loading infrastructure for runtime SVG rasterisation. +//! +//! See `CARD_PLAN.md` for the multi-phase implementation plan. This module +//! is the entry point for Phase 1 (the SVG → `Image` asset loader). Later +//! phases extend it with custom asset sources for embedded and user +//! themes, and a `CardTheme` asset that aggregates 53 image handles. + +pub mod svg_loader; + +pub use svg_loader::{rasterize_svg, SvgLoader, SvgLoaderError, SvgLoaderSettings}; diff --git a/solitaire_engine/src/assets/svg_loader.rs b/solitaire_engine/src/assets/svg_loader.rs new file mode 100644 index 0000000..1455aed --- /dev/null +++ b/solitaire_engine/src/assets/svg_loader.rs @@ -0,0 +1,207 @@ +//! Bevy `AssetLoader` that rasterises an SVG into `bevy::image::Image`. +//! +//! The card-theme system (see `CARD_PLAN.md`) ships SVG sources both as +//! the embedded default theme and as user-supplied themes. Bevy 0.18 has +//! no built-in SVG support, so this loader bridges `usvg` (parser) + +//! `resvg` (renderer) + `tiny-skia` (CPU pixmap) to produce textures +//! that the rest of the engine consumes as plain `Handle` — no +//! awareness of vector graphics leaks past this boundary. +//! +//! Rasterisation happens once per (asset, settings) pair at load time. +//! Bevy's asset system caches the resulting `Image`, so the cost is paid +//! exactly once per theme switch, not per frame. +//! +//! # Settings +//! +//! Each `Handle` produced via this loader carries +//! [`SvgLoaderSettings`]. The most important field is `target_size` — +//! callers should specify the rasterisation resolution explicitly when +//! loading via `load_with_settings(...)`. The default of 512×768 is a +//! safe fallback that fits a typical 2:3 playing card. + +use bevy::asset::io::Reader; +use bevy::asset::{AssetLoader, LoadContext, RenderAssetUsages}; +use bevy::image::Image; +use bevy::math::UVec2; +use bevy::reflect::TypePath; +use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Per-asset settings consumed by [`SvgLoader::load`]. +/// +/// `target_size` controls the rasterisation resolution. SVG content is +/// scaled uniformly to fit this box while preserving aspect ratio. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct SvgLoaderSettings { + /// Output texture dimensions in pixels. + pub target_size: UVec2, +} + +impl Default for SvgLoaderSettings { + fn default() -> Self { + // 512×768 is a 2:3 aspect at a resolution that stays sharp on + // typical desktop windows where individual cards never exceed + // ~250 px wide. Callers that need higher fidelity should + // override via `load_with_settings`. + Self { + target_size: UVec2::new(512, 768), + } + } +} + +/// Errors surfaced by [`SvgLoader::load`]. +#[derive(Debug, Error)] +pub enum SvgLoaderError { + /// The asset reader failed before the SVG bytes were consumed. + #[error("io: {0}")] + Io(#[from] std::io::Error), + /// `usvg` rejected the input as malformed or unsupported. + #[error("svg parse: {0}")] + Parse(#[from] usvg::Error), + /// `tiny_skia::Pixmap::new` returned `None` — typically because the + /// requested target_size is zero or absurdly large. + #[error("could not allocate pixmap of size {0}x{1}")] + PixmapAlloc(u32, u32), +} + +/// `AssetLoader` registered for the `.svg` extension. +/// +/// Stateless; safe to construct via `Default` and register once at +/// startup with `app.register_asset_loader(SvgLoader)`. +#[derive(Debug, Default, TypePath)] +pub struct SvgLoader; + +impl AssetLoader for SvgLoader { + type Asset = Image; + type Settings = SvgLoaderSettings; + type Error = SvgLoaderError; + + async fn load( + &self, + reader: &mut dyn Reader, + settings: &Self::Settings, + _load_context: &mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + rasterize_svg(&bytes, settings.target_size) + } + + fn extensions(&self) -> &[&str] { + &["svg"] + } +} + +/// Rasterises an SVG byte buffer into an `Image` of exactly +/// `target.x × target.y` pixels. Content is scaled uniformly to fit +/// while preserving aspect ratio; unused area is left transparent. +/// +/// Exposed separately from the `AssetLoader` impl so callers (tests, +/// the Phase 7 zip importer's "is this a valid SVG?" check, future +/// thumbnail generators) can rasterise without going through the +/// asset graph. +pub fn rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result { + let opt = usvg::Options::default(); + let tree = usvg::Tree::from_data(svg_bytes, &opt)?; + + let svg_size = tree.size(); + let svg_w = svg_size.width(); + let svg_h = svg_size.height(); + + let target_w = target.x as f32; + let target_h = target.y as f32; + + // Scale-to-fit while preserving aspect — the smaller axis ratio wins + // so the entire SVG is visible inside the target box. + let scale = (target_w / svg_w).min(target_h / svg_h); + + let mut pixmap = tiny_skia::Pixmap::new(target.x, target.y) + .ok_or(SvgLoaderError::PixmapAlloc(target.x, target.y))?; + + // Centre the scaled SVG inside the target box so any aspect-ratio + // mismatch is balanced rather than pinned to the top-left corner. + let dx = (target_w - svg_w * scale) * 0.5; + let dy = (target_h - svg_h * scale) * 0.5; + let transform = tiny_skia::Transform::from_scale(scale, scale).post_translate(dx, dy); + + resvg::render(&tree, transform, &mut pixmap.as_mut()); + + Ok(Image::new( + Extent3d { + width: target.x, + height: target.y, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + pixmap.take(), + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::default(), + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Minimal but non-trivial SVG: yellow rectangle + dark circle. + /// Embedded inline so tests have no filesystem dependencies. The + /// `##` raw-string delimiter lets us inline `#`-prefixed hex colours. + const TEST_SVG: &[u8] = br##" + + + +"##; + + #[test] + fn rasterizes_at_default_size() { + let settings = SvgLoaderSettings::default(); + let image = rasterize_svg(TEST_SVG, settings.target_size).expect("rasterisation"); + assert_eq!(image.size().x, 512); + assert_eq!(image.size().y, 768); + } + + #[test] + fn rasterizes_at_custom_size() { + let image = rasterize_svg(TEST_SVG, UVec2::new(64, 96)).expect("rasterisation"); + assert_eq!(image.size().x, 64); + assert_eq!(image.size().y, 96); + } + + #[test] + fn rejects_zero_dimension() { + let err = rasterize_svg(TEST_SVG, UVec2::new(0, 100)).unwrap_err(); + assert!(matches!(err, SvgLoaderError::PixmapAlloc(0, 100))); + } + + #[test] + fn rejects_malformed_svg() { + let err = rasterize_svg(b"not actually svg", UVec2::new(64, 96)).unwrap_err(); + assert!(matches!(err, SvgLoaderError::Parse(_))); + } + + #[test] + fn pixmap_data_is_rgba_with_target_byte_count() { + let image = + rasterize_svg(TEST_SVG, UVec2::new(32, 48)).expect("rasterisation"); + let pixels = image.data.as_ref().expect("rasterised image carries pixel data"); + // 32 × 48 × 4 (RGBA bytes) = 6144 bytes + assert_eq!(pixels.len(), 32 * 48 * 4); + } + + #[test] + fn loader_advertises_svg_extension() { + let loader = SvgLoader; + assert_eq!(loader.extensions(), &["svg"]); + } + + /// Compile-time guard that `SvgLoaderSettings` satisfies the trait + /// bounds Bevy expects on `AssetLoader::Settings` — keeps the + /// loader's `#[derive]` set honest if the upstream signature ever + /// tightens. + #[test] + fn settings_satisfies_loader_bounds() { + fn assert_loader_settings() {} + assert_loader_settings::(); + } +} diff --git a/solitaire_engine/src/lib.rs b/solitaire_engine/src/lib.rs index 01d7b98..98e3c8f 100644 --- a/solitaire_engine/src/lib.rs +++ b/solitaire_engine/src/lib.rs @@ -1,5 +1,6 @@ //! Bevy integration layer for Solitaire Quest. +pub mod assets; pub mod card_animation; pub mod achievement_plugin; pub mod animation_plugin;