build.rs
21.2 kB · rust · 641 lines
1use crate::gen::draw::ConfigNd;2use crate::gen::recipe::{Group, Tile};34/// The constraints a random flat tile is drawn under.5pub type Config2d = ConfigNd<2>;67/// The constraints a random cube tile is drawn under, shared by the hex pipeline.8pub type Config3d = ConfigNd<3>;910pub use six::{build as build_6d, create as create_6d, random_tile as random_tile_6d, HexTile};11pub use three::{build as build_3d, create as create_3d, random_tile as random_tile_3d};12pub use two::{build as build_2d, create as create_2d, random_tile as random_tile_2d};1314// SLOTS1516fn ragged(tile: &Tile) -> bool {17 let slots = tile.sources.len();18 let wanted = match tile.group {19 Group::Mosaic => 3,20 _ => 1,21 };22 slots < wanted23 || tile.numbers.len() < slots24 || tile.levels.len() < slots25 || tile.rotations.len() < slots26}2728// TWO2930mod two {31 use super::Config2d as Config;32 use crate::core::error::{value_error, Result};33 use crate::core::rng::Rng;34 use crate::core::tensor::Tensor;35 use crate::gen::draw as spec;36 use crate::gen::recipe::{Group, Source, Tile};37 use crate::math::bang::Code;38 use crate::math::two::{designs, geometry, Cell2d};3940 fn rotation(rng: &mut Rng) -> usize {41 rng.below(4)42 }4344 /// Draws a random flat tile from the stream, rotations from the four quarter-turns.45 ///46 /// # Errors47 ///48 /// Errs when no allowed group fits the size constraints, or the catalog holds no source.49 pub fn create(config: &Config, rng: &mut Rng) -> Result<Tile> {50 spec::create(config, rotation, rng)51 }5253 /// Draws a random flat tile up to the given size under the default config.54 ///55 /// # Errors56 ///57 /// Errs when no group fits a tile inside the size, or the catalog holds no source.58 pub fn random_tile(max_size: usize, rng: &mut Rng) -> Result<Tile> {59 spec::random_tile::<2>(max_size, rotation, rng)60 }6162 fn source_cell(source: Source, number: usize, level: usize, rotation: usize) -> Result<Cell2d> {63 match source {64 Source::Classic(design) => designs::named(design, number, level, rotation),65 Source::Code(code) => designs::create(Code::from(code), number, level, rotation, 2),66 }67 }6869 fn cell(tile: &Tile, i: usize, level: usize) -> Result<Cell2d> {70 let mut c = source_cell(tile.sources[i], tile.numbers[i], level, tile.rotations[i])?;71 if tile.anti.get(i).copied().unwrap_or(false) {72 c = c.anti();73 }74 Ok(c)75 }7677 fn tree_mask(n: usize) -> Result<Tensor> {78 let vertical = designs::vtree(n, 1)?;79 let horizontal = vertical.clone().rotate(1)?;80 let v = vertical.types();81 let h = horizontal.types();82 let mut data = vec![0u8; v.size()];83 for (flat, item) in data.iter_mut().enumerate() {84 let a = v.at(flat);85 let b = h.at(flat);86 *item = match (a, b) {87 (1, 1) => 2,88 (1, _) | (_, 1) => 1,89 _ => 0,90 };91 }92 Tensor::of(data, v.shape.clone())93 }9495 fn build_general(tile: &Tile) -> Result<Cell2d> {96 cell(tile, 0, 1)97 }9899 fn build_fractal(tile: &Tile) -> Result<Cell2d> {100 cell(tile, 0, tile.levels[0])101 }102103 fn build_magic(tile: &Tile) -> Result<Cell2d> {104 let cells: Result<Vec<Cell2d>> =105 (0..tile.sources.len()).map(|i| cell(tile, i, 1)).collect();106 geometry::magic(&cells?)107 }108109 fn build_special(tile: &Tile) -> Result<Cell2d> {110 let cell = designs::vtree(tile.numbers[0], 1)?;111 let mut mask = source_cell(tile.sources[0], tile.factor, 1, tile.rotations[0])?;112 if tile.flip {113 mask = mask.invert();114 }115 geometry::special(mask.types(), &cell)116 }117118 fn build_mosaic(tile: &Tile) -> Result<Cell2d> {119 let mask = tree_mask(tile.factor)?;120 let cells: Result<Vec<Cell2d>> = (0..3).map(|i| cell(tile, i, 1)).collect();121 geometry::mosaic(&mask, &cells?)122 }123124 fn builder(group: Group) -> fn(&Tile) -> Result<Cell2d> {125 match group {126 Group::General => build_general,127 Group::Fractal => build_fractal,128 Group::Magic => build_magic,129 Group::Special => build_special,130 Group::Mosaic => build_mosaic,131 }132 }133134 /// Builds the flat cell the tile describes.135 ///136 /// ```137 /// use mrlyrs::core::rng::Rng;138 /// use mrlyrs::gen::build::{build_2d, random_tile_2d};139 /// let mut rng = Rng::new(1);140 /// let tile = random_tile_2d(9, &mut rng)?;141 /// assert_eq!(build_2d(&tile)?.width(), tile.width);142 /// # Ok::<(), mrlyrs::Error>(())143 /// ```144 ///145 /// # Errors146 ///147 /// Errs when the tile's slots are ragged, or when a source will not render at its size.148 pub fn build(tile: &Tile) -> Result<Cell2d> {149 if super::ragged(tile) {150 return value_error("tile slots are ragged.");151 }152 let mut c = builder(tile.group)(tile)?;153 if tile.invert {154 c = c.invert();155 }156 Ok(c)157 }158159 #[cfg(test)]160 mod tests {161 use super::*;162 use crate::gen::recipe::{Catalog, Design, Parity};163 #[test]164 fn random_tile_respects_max() {165 for s in 0..50 {166 let mut rng = Rng::new(s);167 let tile = random_tile(30, &mut rng).unwrap();168 assert!(tile.max_size() <= 30);169 }170 }171 #[test]172 fn magic_can_nest_deeper_than_two() {173 let config = Config {174 min_size: 3,175 max_size: 300,176 groups: vec![Group::Magic],177 anti: Some(false),178 ..Config::default()179 };180 let mut deep = false;181 for s in 0..200 {182 let mut rng = Rng::new(s);183 if let Ok(tile) = create(&config, &mut rng) {184 if tile.sources.len() >= 3 {185 deep = true;186 build(&tile).unwrap();187 }188 }189 }190 assert!(deep, "expected at least one magic tile nested 3+ deep");191 }192 #[test]193 fn magic_rolls_never_repeat_a_fractal() {194 let config = Config {195 catalog: Catalog::Codes(vec![7]),196 min_size: 3,197 max_size: 64,198 groups: vec![Group::Magic],199 anti: Some(false),200 ..Config::default()201 };202 for s in 0..200 {203 let mut rng = Rng::new(s);204 let tile = create(&config, &mut rng).unwrap();205 assert!(tile.sources.len() >= 2, "seed {s} rolled one slot");206 assert!(!tile.degenerate(), "seed {s} rolled a fractal twin");207 let cell = build(&tile).unwrap();208 assert_eq!(cell.width(), tile.width, "seed {s}");209 }210 }211 #[test]212 fn a_magic_roll_keeps_its_twin_when_nothing_else_fits() {213 let config = Config {214 catalog: Catalog::Codes(vec![7]),215 min_size: 9,216 max_size: 9,217 groups: vec![Group::Magic],218 anti: Some(false),219 ..Config::default()220 };221 for s in 0..20 {222 let mut rng = Rng::new(s);223 let tile = create(&config, &mut rng).unwrap();224 assert_eq!(tile.numbers, vec![3, 3], "seed {s}");225 assert!(tile.degenerate(), "seed {s}");226 assert_eq!(build(&tile).unwrap().width(), 9, "seed {s}");227 }228 }229 #[test]230 fn refuses_a_flat_tile_it_cannot_build() {231 use crate::core::json;232 let parsed: Tile = serde_json::from_value(json!({233 "group": "General", "factor": 0,234 "sources": [{ "design": "Carpet" }],235 "numbers": [], "levels": [], "rotations": [], "anti": [],236 "invert": false, "flip": false, "width": 0, "height": 0,237 }))238 .unwrap();239 assert!(build(&parsed).is_err());240 let mut bare = Tile::new(Group::Mosaic);241 bare.sources = vec![Source::Classic(Design::Carpet)];242 assert!(build(&bare).is_err());243 assert!(build(&Tile::new(Group::General)).is_err());244 let mut cubic = Tile::new(Group::General);245 cubic.sources = vec![Source::Classic(Design::Xtree)];246 cubic.numbers = vec![3];247 cubic.levels = vec![1];248 cubic.rotations = vec![0];249 cubic.anti = vec![false];250 cubic.resize();251 assert!(build(&cubic).is_err());252 }253 #[test]254 fn evens_parity_builds() {255 let config = Config {256 min_size: 4,257 max_size: 64,258 parity: Parity::Evens,259 groups: vec![Group::General],260 anti: Some(false),261 ..Config::default()262 };263 for s in 0..50 {264 let mut rng = Rng::new(s);265 let tile = create(&config, &mut rng).unwrap();266 assert_eq!(tile.numbers[0] % 2, 0);267 let cell = build(&tile).unwrap();268 assert_eq!(cell.width(), tile.width);269 }270 }271 }272}273274// THREE275276mod three {277 use super::Config3d as Config;278 use crate::core::error::{value_error, Result};279 use crate::core::rng::Rng;280 use crate::core::tensor::Tensor;281 use crate::gen::draw as spec;282 use crate::gen::recipe::{Design, Group, Source, Tile};283 use crate::math::bang::Code;284 use crate::math::three::{designs, geometry, Cell3d};285286 fn rotation(rng: &mut Rng) -> usize {287 rng.below(24)288 }289290 /// Draws a cube tile from the config with cube orientations drawn from the stream.291 ///292 /// # Errors293 ///294 /// Errs when no allowed group fits the size constraints, or the catalog holds no source.295 pub fn create(config: &Config, rng: &mut Rng) -> Result<Tile> {296 spec::create(config, rotation, rng)297 }298299 /// Draws a random cube tile up to the given size.300 ///301 /// # Errors302 ///303 /// Errs when no group fits a tile inside the size, or the catalog holds no source.304 pub fn random_tile(max_size: usize, rng: &mut Rng) -> Result<Tile> {305 spec::random_tile::<3>(max_size, rotation, rng)306 }307308 fn design_cell(design: Design, number: usize, level: usize) -> Result<Cell3d> {309 match design {310 Design::Carpet => designs::carpet(number, level),311 Design::Net => designs::net(number, level),312 Design::Xtree => designs::xtree(number, level),313 Design::Ytree => designs::ytree(number, level),314 Design::Ztree => designs::ztree(number, level),315 Design::Void => designs::void(number, level),316 Design::Point => designs::point(number, level),317 Design::Dust => designs::dust(number, level),318 Design::Xline => designs::xline(number, level),319 Design::Yline => designs::yline(number, level),320 Design::Zline => designs::zline(number, level),321 Design::Star => designs::star(number, level),322 other => value_error(format!("design {} is not 3d.", other.name())),323 }324 }325326 fn source_cell(source: Source, number: usize, level: usize, rotation: usize) -> Result<Cell3d> {327 let mut c = match source {328 Source::Classic(design) => design_cell(design, number, level)?,329 Source::Code(code) => designs::create(Code::from(code), number, level, 2)?,330 };331 if rotation != 0 {332 c = c.orient(rotation)?;333 }334 Ok(c)335 }336337 fn cell(tile: &Tile, i: usize, level: usize) -> Result<Cell3d> {338 let mut c = source_cell(tile.sources[i], tile.numbers[i], level, tile.rotations[i])?;339 if tile.anti.get(i).copied().unwrap_or(false) {340 c = c.anti();341 }342 Ok(c)343 }344345 fn orient_mask(n: usize, fill: u8) -> Result<Tensor> {346 let line = designs::xtree(n, 1)?;347 let t = line.types();348 let data: Vec<u8> = t349 .bytes()?350 .iter()351 .map(|&v| if v == 1 { fill } else { 0 })352 .collect();353 Tensor::of(data, t.shape.clone())354 }355356 fn index_mask(n: usize) -> Result<Tensor> {357 let x = designs::xtree(n, 1)?;358 let y = designs::ytree(n, 1)?;359 let z = designs::ztree(n, 1)?;360 let (xt, yt, zt) = (x.types(), y.types(), z.types());361 let mut data = vec![0u8; xt.size()];362 for (flat, item) in data.iter_mut().enumerate() {363 *item = if zt.at(flat) == 1 {364 2365 } else if yt.at(flat) == 1 {366 1367 } else {368 0369 };370 }371 Tensor::of(data, xt.shape.clone())372 }373374 fn build_general(tile: &Tile) -> Result<Cell3d> {375 cell(tile, 0, 1)376 }377378 fn build_fractal(tile: &Tile) -> Result<Cell3d> {379 cell(tile, 0, tile.levels[0])380 }381382 fn build_magic(tile: &Tile) -> Result<Cell3d> {383 let cells: Result<Vec<Cell3d>> =384 (0..tile.sources.len()).map(|i| cell(tile, i, 1)).collect();385 geometry::magic(&cells?)386 }387388 fn build_special(tile: &Tile) -> Result<Cell3d> {389 let cell = designs::xtree(tile.numbers[0], 1)?;390 let fill = if tile.flip {391 0392 } else {393 tile.rotations[0].max(1) as u8394 };395 let mask = orient_mask(tile.factor, fill)?;396 geometry::special(&mask, &cell)397 }398399 fn build_mosaic(tile: &Tile) -> Result<Cell3d> {400 let mask = index_mask(tile.factor)?;401 let cells: Result<Vec<Cell3d>> = (0..3).map(|i| cell(tile, i, 1)).collect();402 geometry::mosaic(&mask, &cells?)403 }404405 fn builder(group: Group) -> fn(&Tile) -> Result<Cell3d> {406 match group {407 Group::General => build_general,408 Group::Fractal => build_fractal,409 Group::Magic => build_magic,410 Group::Special => build_special,411 Group::Mosaic => build_mosaic,412 }413 }414415 /// Builds the cube the tile describes.416 ///417 /// # Errors418 ///419 /// Errs when the tile's slots are ragged, or when a source is flat or will not render.420 pub fn build(tile: &Tile) -> Result<Cell3d> {421 if super::ragged(tile) {422 return value_error("tile slots are ragged.");423 }424 let mut c = builder(tile.group)(tile)?;425 if tile.invert {426 c = c.invert();427 }428 Ok(c)429 }430431 #[cfg(test)]432 mod tests {433 use super::*;434 use crate::gen::recipe::Catalog;435 fn config() -> Config {436 Config {437 min_size: 3,438 max_size: 27,439 anti: Some(false),440 ..Config::default()441 }442 }443 #[test]444 fn built_size_matches_unit_size() {445 let config = config();446 for s in 0..200 {447 let mut rng = Rng::new(s);448 let tile = create(&config, &mut rng).unwrap();449 let cell = build(&tile).unwrap();450 assert_eq!(451 cell.width(),452 tile.width,453 "width seed {} {:?}",454 s,455 tile.group456 );457 assert_eq!(458 cell.height(),459 tile.height,460 "height seed {} {:?}",461 s,462 tile.group463 );464 assert_eq!(cell.depth(), tile.width, "depth cubic seed {}", s);465 }466 }467 #[test]468 fn create_replays_its_seed() {469 let a = create(&config(), &mut Rng::new(321)).unwrap();470 let b = create(&config(), &mut Rng::new(321)).unwrap();471 assert_eq!(a, b);472 assert_ne!(a, create(&config(), &mut Rng::new(322)).unwrap());473 }474 #[test]475 fn classics_use_named_designs() {476 let config = config();477 for s in 0..50 {478 let mut rng = Rng::new(s);479 let tile = create(&config, &mut rng).unwrap();480 for source in &tile.sources {481 assert!(matches!(source, Source::Classic(_)));482 }483 }484 }485 #[test]486 fn refuses_a_cube_tile_it_cannot_build() {487 assert!(build(&Tile::new(Group::General)).is_err());488 let mut bare = Tile::new(Group::Mosaic);489 bare.sources = vec![Source::Classic(Design::Carpet)];490 assert!(build(&bare).is_err());491 let mut flat = Tile::new(Group::General);492 flat.sources = vec![Source::Classic(Design::Htree)];493 flat.numbers = vec![3];494 flat.levels = vec![1];495 flat.rotations = vec![0];496 flat.anti = vec![false];497 flat.resize();498 assert!(build(&flat).is_err());499 }500 #[test]501 fn universe_builds_from_codes() {502 let config = Config {503 catalog: Catalog::Universe,504 min_size: 3,505 max_size: 9,506 anti: Some(false),507 ..Config::default()508 };509 for s in 0..60 {510 let mut rng = Rng::new(s);511 let tile = create(&config, &mut rng).unwrap();512 let cell = build(&tile).unwrap();513 assert_eq!(cell.width(), tile.width, "universe width seed {}", s);514 for source in &tile.sources {515 assert!(matches!(source, Source::Code(_)));516 }517 }518 }519 }520}521522// SIX523524mod six {525 use super::three;526 use super::Config3d as Config;527 use crate::core::error::Result;528 use crate::core::rng::Rng;529 use crate::gen::recipe::Tile;530 use crate::math::six::geometry::{cut, iso, pro};531 use crate::math::six::{Cell6d, Projection};532533 /// A cube tile paired with the projection that flattens it.534 #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]535 pub struct HexTile {536 /// The projection that flattens the tile.537 pub projection: Projection,538 /// The cube tile underneath.539 pub tile: Tile,540 }541542 fn projection(rng: &mut Rng) -> Result<Projection> {543 Ok(*rng.choice(&[Projection::Iso, Projection::Pro, Projection::Cut])?)544 }545546 /// Draws a cube tile from the config under a projection drawn from the stream.547 ///548 /// # Errors549 ///550 /// Errs when no allowed group fits the size constraints, or the catalog holds no source.551 pub fn create(config: &Config, rng: &mut Rng) -> Result<HexTile> {552 Ok(HexTile {553 projection: projection(rng)?,554 tile: three::create(config, rng)?,555 })556 }557558 /// Draws a random cube tile up to the given size under a random projection.559 ///560 /// # Errors561 ///562 /// Errs when no group fits a tile inside the size, or the catalog holds no source.563 pub fn random_tile(max_size: usize, rng: &mut Rng) -> Result<HexTile> {564 Ok(HexTile {565 projection: projection(rng)?,566 tile: three::random_tile(max_size, rng)?,567 })568 }569570 /// Builds the tile's cube and flattens it through its projection.571 ///572 /// # Errors573 ///574 /// Errs when the cube will not build, or when the projection will not flatten it.575 pub fn build(hex: &HexTile) -> Result<Cell6d> {576 let cell = three::build(&hex.tile)?;577 match hex.projection {578 Projection::Iso => iso(&cell),579 Projection::Pro => pro(&cell),580 Projection::Cut => cut(&cell),581 }582 }583584 #[cfg(test)]585 mod tests {586 use super::*;587 use crate::gen::recipe::Group;588 fn config() -> Config {589 Config {590 min_size: 3,591 max_size: 9,592 anti: Some(false),593 ..Config::default()594 }595 }596 #[test]597 fn projects_every_group_in_every_projection() {598 let config = config();599 for s in 0..40 {600 let mut rng = Rng::new(s);601 let hex = create(&config, &mut rng).unwrap();602 let cell = build(&hex).unwrap();603 assert!(604 cell.width() > 0,605 "empty width seed {} {:?}",606 s,607 hex.tile.group608 );609 assert!(cell.height() > 0, "empty height seed {}", s);610 }611 }612 #[test]613 fn refuses_a_hex_tile_it_cannot_build() {614 let bare = HexTile {615 projection: Projection::Iso,616 tile: Tile::new(Group::General),617 };618 assert!(build(&bare).is_err());619 }620 #[test]621 fn magic_projects() {622 let config = Config {623 min_size: 3,624 max_size: 15,625 groups: vec![Group::Magic],626 anti: Some(false),627 ..Config::default()628 };629 let mut built = 0;630 for s in 0..30 {631 let mut rng = Rng::new(s);632 if let Ok(hex) = create(&config, &mut rng) {633 let cell = build(&hex).unwrap();634 assert!(cell.width() > 0);635 built += 1;636 }637 }638 assert!(built > 0, "expected magic tiles to project");639 }640 }641}