immense 0.1.4

A library for building 3D structures with simple composable rules.
Documentation
use hex;
use immense::*;
use itertools::iproduct;
use lazy_static::lazy_static;
use noise::{Fbm, NoiseFn};
use palette::encoding::pixel::Pixel;
use palette::encoding::srgb::Srgb;
use palette::rgb::Rgb;
use rand::seq::SliceRandom;
use rand::thread_rng;
use rand::Rng;
use std::fs::File;
use std::io::BufWriter;
use std::rc::Rc;

const SPHERE_RESOLUTION: usize = 0;

lazy_static! {
    static ref PALETTE: [Hsv; 5] = [
        hexcolor("4F4052"),
        hexcolor("6D7577"),
        hexcolor("95A8A9"),
        hexcolor("A8C4BE"),
        hexcolor("AFD8DB"),
    ];
}

fn hexcolor(hex: &str) -> Hsv {
    let bytes: Vec<u8> = hex::decode(hex).expect("raw bytes from hex");
    let fmt = |i| bytes[i] as f32 / 255.0;
    let color: Rgb<Srgb, f32> = *Rgb::from_raw(&[fmt(0), fmt(1), fmt(2)]);
    Hsv::from(color)
}

trait Tilable: Clone + 'static {
    fn to_tile(&self, row: usize, col: usize) -> Rule;
}

struct GridTile<T> {
    row: usize,
    col: usize,
    tilable: T,
}

impl<T: Tilable> ToRule for GridTile<T> {
    fn to_rule(&self) -> Rule {
        self.tilable.to_tile(self.row, self.col)
    }
}

fn grid(rows: usize, cols: usize, rule: impl Tilable) -> Rule {
    rule![
        tf![Tf::tx((cols - 1) as f32 / -2.0), Tf::tz((rows - 1) as f32 / -2.0)] =>
        iproduct!(0..rows, 0..cols).fold(Rule::new(), |root_rule, (r, c)| {
            root_rule.push(tf![
                Tf::tx(c as f32),
                Tf::tz(r as f32)
            ], GridTile {
                row: r,
                col: c,
                tilable: rule.clone()
            })
        })
    ]
}

#[derive(Clone)]
struct Pyramid {
    levels: usize,
    sphere: Rc<Mesh>,
}

impl ToRule for Pyramid {
    fn to_rule(&self) -> Rule {
        (0..self.levels).fold(Rule::new(), |rule, i| {
            let target_downscale = 1.0 - ((i + 1) as f32 / self.levels as f32);
            rule.push(
                tf![
                    Tf::sby(
                        target_downscale,
                        ((1.0 / self.levels as f32) * 1.6).powi(2),
                        target_downscale
                    ),
                    Tf::ty(i as f32),
                    Tf::color(*PALETTE.choose(&mut thread_rng()).unwrap())
                ],
                (*&[cube(), self.sphere.to_rule()]
                    .choose(&mut thread_rng())
                    .unwrap())
                    .clone(),
            )
        })
    }
}

#[derive(Clone)]
struct Tower;

impl ToRule for Tower {
    fn to_rule(&self) -> Rule {
        let thin = 0.002;
        let bars = thread_rng().gen_range(4, 20);
        let height = 0.03 * (1.0 / bars as f32);
        let bar = rule![
            tf![Tf::color(*PALETTE.choose(&mut thread_rng()).unwrap()), Tf::tx(-0.5), Tf::sby(thin, height, 1.0)] => cube(),
            tf![Tf::color(*PALETTE.choose(&mut thread_rng()).unwrap()), Tf::tx(0.5), Tf::sby(thin, height, 1.0)] => cube(),
            tf![Tf::color(*PALETTE.choose(&mut thread_rng()).unwrap()), Tf::tz(-0.5), Tf::sby(1.0, height, thin)] => cube(),
            tf![Tf::color(*PALETTE.choose(&mut thread_rng()).unwrap()), Tf::tz(0.5), Tf::sby(1.0, height, thin)] => cube(),
            tf![Tf::color(*PALETTE.choose(&mut thread_rng()).unwrap()), Tf::ty(0.0), Tf::s(0.2)] => icosphere(),
        ];
        rule![
            Replicate::n(bars, Tf::ty(height * 13.0)) => bar,
        ]
    }
}

#[derive(Clone)]
struct CityBlock {
    sphere: Rc<Mesh>,
    noise: Fbm,
    depth: usize,
}

impl Tilable for CityBlock {
    fn to_tile(&self, row: usize, col: usize) -> Rule {
        let division = (self.noise.get([row as f64, col as f64]).abs() * 10.0) as usize + 4;
        let mut candidates = vec![
            rule![None => Pyramid {
                levels: division,
                sphere: self.sphere.clone(),
            }],
            rule![None => Tower {}],
        ];
        if self.depth < 3 {
            candidates.push(rule![tf![Tf::sby(0.5, 0.5, 0.5)] => grid(
                2,
                2,
                CityBlock {
                    sphere: self.sphere.clone(),
                    noise: self.noise.clone(),
                    depth: self.depth + 1,
                },
            )]);
        }
        let mut rng = thread_rng();
        (&candidates).choose(&mut rng).unwrap().clone()
    }
}

#[derive(Copy, Clone)]
struct Wire;

impl Tilable for Wire {
    fn to_tile(&self, _: usize, _: usize) -> Rule {
        let height = 0.03;
        let thin = 0.05;
        rule![Tf::color(*PALETTE.choose(&mut thread_rng()).unwrap()) => rule![
            tf![Tf::tx(-0.5), Tf::sby(thin, height, 1.0)] => cube(),
            tf![Tf::tx(0.5), Tf::sby(thin, height, 1.0)] => cube(),
            tf![Tf::tz(-0.5), Tf::sby(1.0, height, thin)] => cube(),
            tf![Tf::tz(0.5), Tf::sby(1.0, height, thin)] => cube()
        ]]
    }
}

fn main() {
    let grid_size = 10;
    let meshes = rule![
        tf![
            Tf::sby(grid_size as f32, 1.0, grid_size as f32),
            Tf::ty(-0.5),
            Tf::color(Hsv::new(0.0, 0.0, 1.0))
        ] => cube(),
        None => grid(grid_size, grid_size, CityBlock { sphere: sphere(SPHERE_RESOLUTION), noise: Fbm::new(), depth: 0 }),
        None => grid(grid_size, grid_size, Wire {})
    ]
    .generate();
    let output = File::create("pyramid.obj").expect("obj file");
    write_meshes(
        ExportConfig {
            grouping: MeshGrouping::ByColor,
            export_colors: Some(String::from("pyramid_colors.mtl")),
        },
        meshes,
        &mut BufWriter::new(output),
    )
    .expect("rendered scene");
}