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}