coffee 0.4.1

An opinionated 2D game engine focused on simplicity, explicitness, and type-safety
Documentation
use coffee::graphics::{Canvas, Gpu, Image};

mod mesh;

use mesh::Mesh;

use std::fs::File;
use std::io::Read;
use std::path::PathBuf;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Test {
    Mesh,
}

impl Test {
    pub fn all() -> Vec<Test> {
        vec![Test::Mesh]
    }

    pub fn run(&self, gpu: &mut Gpu) -> Drawing {
        let draw = match self {
            Test::Mesh => Mesh::draw(),
        };

        Drawing {
            test: *self,
            canvas: draw
                .run(gpu)
                .expect(&format!("Run test \"{}\"", self.to_string())),
        }
    }
}

impl std::string::ToString for Test {
    fn to_string(&self) -> String {
        let name = match self {
            Test::Mesh => "mesh",
        };

        String::from(name)
    }
}

pub struct Drawing {
    test: Test,
    canvas: Canvas,
}

impl Drawing {
    pub fn test(&self) -> Test {
        self.test
    }

    pub fn canvas(&self) -> &Canvas {
        &self.canvas
    }

    pub fn save_as_model(&self, gpu: &mut Gpu) {
        let model_path = self.model_path();
        let model_directory = model_path.parent().expect("Get model directory");
        let image = self.canvas.read_pixels(gpu);

        std::fs::create_dir_all(model_directory)
            .expect("Create model directory");

        image
            .to_rgba()
            .save(self.model_path())
            .expect(&format!("Save \"{:?}\" drawing", self.test));
    }

    pub fn differences(
        &self,
        gpu: &mut Gpu,
    ) -> Result<Option<Differences>, Error> {
        let model = {
            let mut buf = Vec::new();
            let mut reader = File::open(self.model_path())?;
            let _ = reader.read_to_end(&mut buf)?;
            image::load_from_memory(&buf)?
        };

        let image = self.canvas.read_pixels(gpu);

        let model_rgba = model.to_rgba();
        let image_rgba = image.to_rgba();

        if model_rgba
            .pixels()
            .zip(image_rgba.pixels())
            .all(|(a, b)| a == b)
        {
            Ok(None)
        } else {
            let differences: Vec<u8> = model_rgba
                .pixels()
                .zip(image_rgba.pixels())
                .flat_map(|(a, b)| {
                    if a == b {
                        &[0, 0, 0, 0]
                    } else {
                        &[255, 0, 0, 255]
                    }
                })
                .cloned()
                .collect();

            let image = image::RgbaImage::from_raw(
                self.canvas.width() as u32,
                self.canvas.height() as u32,
                differences,
            )
            .expect("Create diff image");

            let image =
                Image::from_image(gpu, &image::DynamicImage::ImageRgba8(image))
                    .expect("Upload diff image");

            Ok(Some(Differences {
                test: self.test,
                canvas: self.canvas.clone(),
                image,
            }))
        }
    }

    fn model_path(&self) -> PathBuf {
        let mut path = PathBuf::new();

        path.push("tests");
        path.push("_graphics");
        path.push("models");
        path.push(self.test.to_string());
        path.set_extension("png");

        path
    }
}

#[derive(Debug)]
pub struct Differences {
    test: Test,
    canvas: Canvas,
    image: Image,
}

impl Differences {
    pub fn test(&self) -> Test {
        self.test
    }

    pub fn canvas(&self) -> &Canvas {
        &self.canvas
    }

    pub fn image(&self) -> &Image {
        &self.image
    }
}

#[derive(Debug)]
pub enum Error {
    ModelImageNotFound(std::io::Error),
    ModelImageIsInvalid(image::ImageError),
}

impl From<std::io::Error> for Error {
    fn from(error: std::io::Error) -> Error {
        Error::ModelImageNotFound(error)
    }
}

impl From<image::ImageError> for Error {
    fn from(error: image::ImageError) -> Error {
        Error::ModelImageIsInvalid(error)
    }
}