codecraft 0.1.1

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, an immediate-mode UI, audio and gamepad haptics
Documentation
//! Phosphor icons, rasterised into the atlas the UI draws from.
//!
//! The icons ship as SVG -- 1500 of them under `assets/icons` -- and the UI
//! draws quads. So a path like [`path::CUBE`] is rasterised the first time
//! something asks for it, packed into [`super::atlas`] beside the glyphs, and
//! handed back as the rectangle to sample. Nothing is rasterised until a
//! panel actually asks for it: a scene that shows a dozen icons pays for a
//! dozen, not for fifteen hundred.
//!
//! Every icon is drawn as a **white mask**. Phosphor's strokes are
//! `currentColor`, which usvg resolves to black, and black multiplied by a
//! tint stays black -- so the stroke is rewritten to white before rasterising
//! and the colour comes from whatever draws it. That is what lets one file
//! serve as a warm light in one row and a cool one in the next.

mod embedded;
mod names;
pub use embedded::source;
pub use names::path;

/// Icons of our own, for the shapes Phosphor has none of.
///
/// It ships one weight and every icon in it is an outline, which is right for
/// an icon and wrong for a pointer: a hollow triangle beside a menu reads as a
/// shape somebody put there, not as an arrow saying where the menu came from.
pub mod ours {
    pub const POINTER_UP: &str = "ui/pointer-up.svg";
    pub const POINTER_DOWN: &str = "ui/pointer-down.svg";
    pub const POINTER_LEFT: &str = "ui/pointer-left.svg";
    pub const POINTER_RIGHT: &str = "ui/pointer-right.svg";

    /// All four, for the test that checks they are compiled in.
    pub const ALL: [&str; 4] = [POINTER_UP, POINTER_DOWN, POINTER_LEFT, POINTER_RIGHT];
}


/// An SVG as a square of RGBA8 pixels, `size` on a side.
pub(super) fn rasterize(svg: &[u8], size: u32) -> Option<Vec<u8>> {
    use resvg::{tiny_skia, usvg};

    // See the module note: the mask has to be white for tinting to work.
    let svg = String::from_utf8_lossy(svg).replace("currentColor", "#ffffff");
    let tree = usvg::Tree::from_data(svg.as_bytes(), &usvg::Options::default()).ok()?;
    let mut pixmap = tiny_skia::Pixmap::new(size, size)?;

    // Fit without distorting. Phosphor's canvas is square, so this is really
    // just guarding the odd exception.
    let drawing = tree.size();
    let scale = (size as f32 / drawing.width()).min(size as f32 / drawing.height());
    let transform = tiny_skia::Transform::from_scale(scale, scale).post_translate(
        (size as f32 - drawing.width() * scale) / 2.0,
        (size as f32 - drawing.height() * scale) / 2.0,
    );
    resvg::render(&tree, transform, &mut pixmap.as_mut());

    // tiny-skia composites premultiplied and the UI samples straight alpha.
    // Skip this and every icon gets a dark fringe where it fades out.
    let mut data = Vec::with_capacity((size * size * 4) as usize);
    for pixel in pixmap.pixels() {
        let pixel = pixel.demultiply();
        data.extend_from_slice(&[pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()]);
    }
    Some(data)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ui::atlas::ICON_SIZE;

    fn assets() -> std::path::PathBuf {
        std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("assets")
    }

    /// A path with no file behind it draws as nothing at all -- no warning at
    /// the call site, just an icon that quietly is not there.
    #[test]
    fn every_named_icon_ships() {
        let missing: Vec<&str> = path::ALL
            .iter()
            .filter(|(_, path)| !assets().join(path).is_file())
            .map(|(name, _)| *name)
            .collect();
        assert!(missing.is_empty(), "assets is missing: {missing:?}");
    }

    /// Two names pointing at one file means one of them is a typo that
    /// renders as the wrong -- but perfectly valid-looking -- icon.
    #[test]
    fn no_two_names_share_an_icon() {
        let mut seen = std::collections::HashMap::new();
        for (name, path) in path::ALL {
            if let Some(other) = seen.insert(*path, *name) {
                panic!("{name} and {other} are the same icon");
            }
        }
    }

    /// The whole tinting scheme rests on the raster being a white mask: a
    /// black one multiplies to black whatever colour is asked for.
    #[test]
    fn an_icon_rasterizes_to_a_white_mask() {
        let svg = source(path::CUBE).expect("the cube icon is compiled in");
        let raster = rasterize(svg.as_bytes(), 64).expect("it rasterizes");

        let opaque: Vec<&[u8]> = raster.chunks(4).filter(|p| p[3] > 200).collect();
        assert!(!opaque.is_empty(), "the icon drew nothing");
        for pixel in opaque {
            assert_eq!(
                &pixel[..3],
                &[255, 255, 255],
                "a stroke is not white, so tinting it will not work",
            );
        }
    }

    /// Everything `embedded` names has to be a real file, and every one of
    /// them has to rasterise -- otherwise the failure is an icon that quietly
    /// is not there.
    #[test]
    fn every_embedded_icon_draws_something() {
        for (path, svg) in embedded::ALL {
            let raster = rasterize(svg.as_bytes(), ICON_SIZE)
                .unwrap_or_else(|| panic!("{path} does not rasterise"));
            assert!(
                raster.chunks(4).any(|p| p[3] > 0),
                "{path} rasterises to nothing",
            );
        }
    }
}