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}