variation.rs

14.5 kB · rust · 422 lines

1use crate::core::error::{value_error, Error, Result};2use crate::core::paint::{self as engine, Config as PaintConfig, Edition, Ink, Paint};3use crate::core::rng::Rng;4use crate::gen::build::{build_2d, create_2d, Config2d};5use crate::gen::recipe::Tile;6use crate::math::two;7use serde::{Deserialize, Serialize};89/// One rendering of an artwork, sized in tile repetitions.10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]11pub struct File {12    /// The count of tile repetitions across.13    pub width: usize,14    /// The count of tile repetitions down.15    pub height: usize,16    /// The encoded PNG bytes, empty until rendered and left out of the json.17    #[serde(skip)]18    pub png: Vec<u8>,19}2021impl File {22    /// Builds a file of the given repetition counts with no PNG bytes.23    pub fn new(width: usize, height: usize) -> File {24        File {25            width,26            height,27            png: Vec::new(),28        }29    }30}3132/// The settings an artwork is drawn under.33#[derive(Clone, Debug, Serialize, Deserialize)]34pub struct Config {35    /// The constraints the tile is drawn under.36    pub tile: Config2d,37    /// The constraints the paint is drawn under.38    pub paint: PaintConfig,39    /// The width and height repetition pairs to render.40    pub files: Vec<(usize, usize)>,41}4243impl Default for Config {44    fn default() -> Config {45        Config {46            tile: Config2d::default(),47            paint: PaintConfig::default(),48            files: vec![(1, 1), (3, 3), (5, 5)],49        }50    }51}5253/// One seeded artwork, from tile recipe to rendered files.54#[derive(Clone, Debug, Serialize, Deserialize)]55pub struct Variation {56    /// The random hex identifier.57    pub key: String,58    /// The seed the variation is drawn under.59    pub seed: u64,60    /// The paint edition.61    pub edition: Edition,62    /// The primary inks, when the config fixes them.63    pub primaries: Option<Vec<Ink>>,64    /// The tile recipe.65    pub tile: Tile,66    /// The mask tile, present only under the Neighbors edition.67    pub mask: Option<Tile>,68    /// The paint, set by generate.69    pub paint: Option<Paint>,70    /// The built base cell, set by generate and left out of the json.71    #[serde(skip)]72    pub base: Option<two::Cell2d>,73    /// The renderings, filled by render.74    pub files: Vec<File>,75}7677impl Variation {78    /// Returns whether the edition paints the whole tiled canvas.79    pub fn is_cover(&self) -> bool {80        matches!(81            self.edition,82            Edition::Rows | Edition::Columns | Edition::Random83        )84    }85    /// Returns whether the edition paints the base cell before tiling.86    pub fn is_prime(&self) -> bool {87        matches!(self.edition, Edition::Layers | Edition::Neighbors)88    }89}9091fn hex_key(length: usize, rng: &mut Rng) -> String {92    const DIGITS: &[u8; 16] = b"0123456789abcdef";93    (0..length).map(|_| DIGITS[rng.below(16)] as char).collect()94}9596fn pop_center(tile: &Tile, cell: &mut two::Cell2d) {97    let _ = tile;98    let center = cell.width() / 2;99    let mut types = cell.cell.types.clone();100    types.put(types.index(&[center, center]), 0);101    cell.cell.types = types;102}103104/// Draws a variation's seed from the stream, then the variation itself on that seed, with a105/// mask when the edition is Neighbors.106///107/// ```108/// use mrlyrs::core::rng::Rng;109/// use mrlyrs::gen::variation::{create, Config};110/// let mut rng = Rng::new(1);111/// assert_eq!(create(&Config::default(), &mut rng)?.key.len(), 8);112/// # Ok::<(), mrlyrs::Error>(())113/// ```114///115/// # Errors116///117/// Errs when the tile config draws no tile, or when the Neighbors mask will not draw.118pub fn create(config: &Config, rng: &mut Rng) -> Result<Variation> {119    let s = rng.range(0, i64::MAX) as u64;120    let mut rng = Rng::new(s);121    let edition = engine::random_edition(config.paint.editions.as_deref(), &mut rng);122    let tile = create_2d(&config.tile, &mut rng)?;123    let mask = if edition == Edition::Neighbors {124        let mask_config = Config2d {125            min_size: 3,126            max_size: 3,127            ..Config2d::default()128        };129        Some(create_2d(&mask_config, &mut rng)?)130    } else {131        None132    };133    Ok(Variation {134        key: hex_key(8, &mut rng),135        seed: s,136        edition,137        primaries: config.paint.primaries.clone(),138        tile,139        mask,140        paint: None,141        base: None,142        files: config.files.iter().map(|&(w, h)| File::new(w, h)).collect(),143    })144}145146/// Builds the variation's base cell and draws its paint from the stream, painting the base147/// under a prime edition.148///149/// # Errors150///151/// Errs when the tile or its mask will not build, or when the paint will not lay down.152pub fn generate(mut variation: Variation, config: &Config, rng: &mut Rng) -> Result<Variation> {153    let _ = config;154    let mut base = build_2d(&variation.tile)?;155    let paint_config = PaintConfig {156        editions: Some(vec![variation.edition]),157        primaries: variation.primaries.clone(),158        target: None,159    };160    if variation.is_cover() {161        let p = engine::setup(Paint::new(variation.edition), &paint_config, rng);162        variation.paint = Some(p);163    } else {164        let mask_tensor = match &variation.mask {165            Some(mask_tile) => {166                let mut mask_cell = build_2d(mask_tile)?;167                pop_center(mask_tile, &mut mask_cell);168                Some(mask_cell.cell.types.clone())169            }170            None => None,171        };172        let mut cell = base.cell.clone();173        let p = engine::paint(&mut cell, &paint_config, mask_tensor.as_ref(), rng)?;174        base.cell = cell;175        variation.paint = Some(p);176    }177    variation.base = Some(base);178    Ok(variation)179}180181/// Renders every file of the variation to PNG at the given scale, scattering a Random edition182/// from the stream.183///184/// # Errors185///186/// Errs when generate has not run, or when a file will not tile or encode at the scale.187pub fn render(mut variation: Variation, scale: usize, rng: &mut Rng) -> Result<Variation> {188    let base = match &variation.base {189        Some(base) => base.clone(),190        None => return value_error("call generate before render."),191    };192    let paint = variation193        .paint194        .clone()195        .ok_or_else(|| Error::Value("call generate before render.".into()))?;196    let cover = variation.is_cover();197    let mut files = std::mem::take(&mut variation.files);198    for file in files.iter_mut() {199        let mut canvas = base.clone().tile(file.width, file.height)?;200        if cover {201            let mut cell = canvas.cell.clone();202            engine::apply(&paint, &mut cell, rng)?;203            canvas.cell = cell;204        }205        file.png = two::png(&canvas, scale)?;206    }207    variation.files = files;208    Ok(variation)209}210211#[cfg(test)]212mod tests {213    use super::*;214    use crate::core::json;215    use crate::gen::recipe::{Catalog, Parity};216    fn round_trip(variation: &Variation) -> Variation {217        serde_json::from_value(serde_json::to_value(variation).unwrap()).unwrap()218    }219    fn config() -> Config {220        Config {221            tile: Config2d {222                min_size: 3,223                max_size: 27,224                anti: Some(false),225                ..Config2d::default()226            },227            paint: PaintConfig::default(),228            files: vec![(1, 1), (3, 3)],229        }230    }231    fn edition_config(edition: Edition) -> Config {232        Config {233            paint: PaintConfig {234                editions: Some(vec![edition]),235                ..PaintConfig::default()236            },237            ..config()238        }239    }240    fn png_size(png: &[u8]) -> (usize, usize) {241        let w = u32::from_be_bytes(png[16..20].try_into().unwrap()) as usize;242        let h = u32::from_be_bytes(png[20..24].try_into().unwrap()) as usize;243        (w, h)244    }245    fn bare_png(variation: &Variation, file: &File, scale: usize) -> Vec<u8> {246        let base = variation.base.as_ref().unwrap();247        let bare = two::Cell2d::new(base.types().clone())248            .unwrap()249            .tile(file.width, file.height)250            .unwrap();251        two::png(&bare, scale).unwrap()252    }253    fn run(config: &Config, seed: u64, scale: usize) -> Variation {254        let mut rng = Rng::new(seed);255        let v = create(config, &mut rng).unwrap();256        let v = generate(v, config, &mut rng).unwrap();257        render(v, scale, &mut rng).unwrap()258    }259    #[test]260    fn full_pipeline_produces_png_bytes() {261        for s in 0..20 {262            let v = run(&config(), s, 4);263            assert_eq!(v.files.len(), 2);264            for file in &v.files {265                assert!(266                    !file.png.is_empty(),267                    "empty png for {}x{}",268                    file.width,269                    file.height270                );271                assert_eq!(&file.png[1..4], b"PNG", "not a png header");272                let expected = (273                    v.tile.width * file.width * 4,274                    v.tile.height * file.height * 4,275                );276                assert_eq!(png_size(&file.png), expected, "png size seed {s}");277            }278        }279    }280    #[test]281    fn variation_replays_its_seed() {282        let a = create(&config(), &mut Rng::new(42)).unwrap();283        let b = create(&config(), &mut Rng::new(42)).unwrap();284        assert_eq!(a.seed, b.seed);285        assert_eq!(a.key, b.key);286        assert_eq!(a.tile, b.tile);287        assert_eq!(a.edition, b.edition);288        assert_ne!(a.seed, create(&config(), &mut Rng::new(43)).unwrap().seed);289    }290    #[test]291    fn editions_keep_their_palette() {292        for (i, edition) in Edition::all().into_iter().enumerate() {293            let config = edition_config(edition);294            let mut differed = false;295            for s in 0..8 {296                let v = run(&config, 1000 * (i as u64 + 1) + s, 2);297                if v.files.iter().all(|f| f.png != bare_png(&v, f, 2)) {298                    differed = true;299                    break;300                }301            }302            assert!(differed, "edition {edition:?} never rendered its palette");303        }304    }305    #[test]306    fn layers_edition_keeps_its_palette() {307        let config = edition_config(Edition::Layers);308        let mut rng = Rng::new(7);309        let v = create(&config, &mut rng).unwrap();310        assert_eq!(v.edition, Edition::Layers);311        let v = generate(v, &config, &mut rng).unwrap();312        assert!(v.base.as_ref().unwrap().cell.colors.is_some());313        let v = render(v, 2, &mut rng).unwrap();314        for file in &v.files {315            assert_ne!(file.png, bare_png(&v, file, 2), "default mapping leaked");316        }317    }318    #[test]319    fn neighbors_edition_gets_a_mask() {320        let config = edition_config(Edition::Neighbors);321        let mut rng = Rng::new(3);322        let v = create(&config, &mut rng).unwrap();323        assert_eq!(v.edition, Edition::Neighbors);324        assert!(v.mask.is_some());325        let v = generate(v, &config, &mut rng).unwrap();326        assert!(v.base.as_ref().unwrap().cell.colors.is_some());327        let v = render(v, 2, &mut rng).unwrap();328        for file in &v.files {329            assert_ne!(file.png, bare_png(&v, file, 2), "default mapping leaked");330        }331    }332    #[test]333    fn neighbors_mask_builds_under_evens_parity() {334        let config = Config {335            tile: Config2d {336                min_size: 4,337                max_size: 16,338                parity: Parity::Evens,339                anti: Some(false),340                ..Config2d::default()341            },342            paint: PaintConfig {343                editions: Some(vec![Edition::Neighbors]),344                ..PaintConfig::default()345            },346            files: vec![(1, 1)],347        };348        for s in 0..10 {349            let mut rng = Rng::new(s);350            let v = create(&config, &mut rng).unwrap();351            let mask = v.mask.as_ref().unwrap();352            assert_eq!((mask.width, mask.height), (3, 3));353            let v = generate(v, &config, &mut rng).unwrap();354            let v = render(v, 2, &mut rng).unwrap();355            assert!(!v.files[0].png.is_empty());356        }357    }358    #[test]359    fn json_round_trips_the_record() {360        for (i, edition) in Edition::all().into_iter().enumerate() {361            let config = edition_config(edition);362            let a = create(&config, &mut Rng::new(500 + i as u64)).unwrap();363            let b = round_trip(&a);364            assert_eq!(b.key, a.key);365            assert_eq!(b.seed, a.seed);366            assert_eq!(b.edition, a.edition);367            assert_eq!(b.primaries, a.primaries);368            assert_eq!(b.tile, a.tile);369            assert_eq!(b.mask, a.mask);370            assert_eq!(b.paint, a.paint);371            let mut ra = Rng::new(a.seed);372            let a = generate(a, &config, &mut ra).unwrap();373            let a = render(a, 2, &mut ra).unwrap();374            let mut rb = Rng::new(b.seed);375            let b = generate(b, &config, &mut rb).unwrap();376            let b = render(b, 2, &mut rb).unwrap();377            assert_eq!(a.paint, b.paint);378            for (fa, fb) in a.files.iter().zip(&b.files) {379                assert_eq!((fa.width, fa.height), (fb.width, fb.height));380                assert!(!fa.png.is_empty());381                assert_eq!(fa.png, fb.png, "edition {edition:?}");382            }383            let c = round_trip(&a);384            assert_eq!(c.paint, a.paint);385            assert!(c.base.is_none());386            assert!(c.files.iter().all(|f| f.png.is_empty()));387        }388    }389    #[test]390    fn refuses_a_config_or_a_record_it_cannot_draw() {391        let mut rng = Rng::new(5);392        let narrow = Config {393            tile: Config2d {394                min_size: 9,395                max_size: 3,396                ..Config2d::default()397            },398            ..config()399        };400        assert!(create(&narrow, &mut rng).is_err());401        let sourceless = Config {402            tile: Config2d {403                catalog: Catalog::Codes(Vec::new()),404                ..Config2d::default()405            },406            ..config()407        };408        assert!(create(&sourceless, &mut rng).is_err());409        let drawn = create(&config(), &mut rng).unwrap();410        assert!(render(drawn, 1, &mut rng).is_err());411        assert!(serde_json::from_value::<Variation>(json!({})).is_err());412        assert!(serde_json::from_value::<File>(json!({ "width": 2 })).is_err());413    }414    #[test]415    fn json_round_trips_tile() {416        let v = create(&config(), &mut Rng::new(5)).unwrap();417        let json = serde_json::to_value(&v).unwrap();418        assert!(json.get("base").is_none());419        let back: Tile = serde_json::from_value(json["tile"].clone()).unwrap();420        assert_eq!(back, v.tile);421    }422}