tile.rs

10.8 kB · rust · 362 lines

1#![allow(clippy::too_many_arguments)]23use crate::{code_of, Fault, Grid};4use mrlyrs::core::json;5use mrlyrs::math::bang::Code;6use mrlyrs::math::six::{self, Cell6d};7use mrlyrs::math::three::{self, Cell3d};8use mrlyrs::math::two::{self, Cell2d};9use wasm_bindgen::prelude::*;1011const PLANE_CELLS: usize = 262_144;12const SOLID_CELLS: usize = 1_000_000;13const SOLID_FILLS: usize = 150_000;14const HEX_TRIANGLES: usize = 200_000;15const HEX_SIDE: usize = 81;16const WALK_CELLS: usize = 150_000;17const WALK_TRIANGLES: usize = 80_000;1819fn rep(value: u32) -> Result<usize, Fault> {20    match (1..=32).contains(&value) {21        true => Ok(value as usize),22        false => Err(Fault::new(23            "a repetition count runs from one to thirty two.",24        )),25    }26}2728fn reps_of(reps: &[u32], axes: usize) -> Result<Vec<usize>, Fault> {29    if reps.len() != axes {30        return Err(Fault::new(format!(31            "a {axes}-axis tiling wants {axes} repetition counts."32        )));33    }34    reps.iter().map(|&value| rep(value)).collect()35}3637fn budget(count: usize, limit: usize, what: &str) -> Result<(), Fault> {38    match count > limit {39        true => Err(Fault::new(format!(40            "{count} {what} is more than this page draws; lower the level or the repeats."41        ))),42        false => Ok(()),43    }44}4546fn side_of(number: usize, level: usize) -> Result<usize, Fault> {47    number48        .checked_pow(level as u32)49        .ok_or_else(|| Fault::new("that side is past what a page draws."))50}5152// PLANE5354fn plane(55    code: &str,56    number: usize,57    level: usize,58    base: usize,59    reps: &[usize],60) -> Result<(Cell2d, Cell2d), Fault> {61    let cell = two::create(Code::from(code_of(code)?), number, level, 0, base)?;62    budget(63        cell.width() * reps[0] * cell.height() * reps[1],64        PLANE_CELLS,65        "cells",66    )?;67    let sheet = cell.clone().tile(reps[0], reps[1])?;68    Ok((cell, sheet))69}7071/// Builds the flat design the code names repeated into a wide-by-high array of copies, as a byte grid.72#[wasm_bindgen]73pub fn tile_grid(74    code: &str,75    number: usize,76    level: usize,77    base: usize,78    wide: u32,79    high: u32,80) -> Result<Grid, Fault> {81    let reps = reps_of(&[wide, high], 2)?;82    let (_, sheet) = plane(code, number, level, base, &reps)?;83    Ok(Grid {84        width: sheet.width() as u32,85        height: sheet.height() as u32,86        types: sheet.types().bytes()?.to_vec(),87    })88}8990// SOLID9192fn solid(93    code: &str,94    number: usize,95    level: usize,96    base: usize,97    reps: &[usize],98) -> Result<(Cell3d, Cell3d), Fault> {99    let cell = three::create(Code::from(code_of(code)?), number, level, base)?;100    budget(101        cell.width() * reps[0] * cell.height() * reps[1] * cell.depth() * reps[2],102        SOLID_CELLS,103        "cells",104    )?;105    let sheet = cell.clone().tile(reps[0], reps[1], reps[2])?;106    Ok((cell, sheet))107}108109/// Lists the filled sites of the cube design repeated into a wide-by-high-by-deep array of copies, as x, y, z triples.110#[wasm_bindgen]111pub fn tile_cells(112    code: &str,113    number: usize,114    level: usize,115    base: usize,116    wide: u32,117    high: u32,118    deep: u32,119) -> Result<Vec<u32>, Fault> {120    let reps = reps_of(&[wide, high, deep], 3)?;121    let (_, sheet) = solid(code, number, level, base, &reps)?;122    let grid = sheet.types();123    let (cols, deep) = (grid.shape[1], grid.shape[2]);124    budget(three::fills(&sheet), SOLID_FILLS, "cubes")?;125    let mut out = Vec::new();126    for (flat, &site) in grid.bytes()?.iter().enumerate() {127        if site != 0 {128            out.extend([129                (flat / (cols * deep)) as u32,130                (flat / deep % cols) as u32,131                (flat % deep) as u32,132            ]);133        }134    }135    Ok(out)136}137138// HEXAGON139140fn hexagon(141    code: &str,142    number: usize,143    level: usize,144    base: usize,145    projection: &str,146) -> Result<Cell6d, Fault> {147    let code = code_of(code)?;148    budget(side_of(number, level)?, HEX_SIDE, "cells to a side")?;149    Ok(match projection {150        "pro" => six::pro_design(Code::from(code), number, level, base)?,151        "cut" => six::cut_design(Code::from(code), number, level, base)?,152        _ => six::iso_design(Code::from(code), number, level, base)?,153    })154}155156fn hex_sheet(157    code: &str,158    number: usize,159    level: usize,160    base: usize,161    projection: &str,162    reps: &[usize],163    crop: bool,164) -> Result<(Cell6d, Cell6d), Fault> {165    let hex = hexagon(code, number, level, base, projection)?;166    budget(167        hex.width() * reps[0] * hex.height() * reps[1],168        HEX_TRIANGLES,169        "triangles",170    )?;171    if crop && (reps[0] < 2 || reps[1] < 2) {172        return Err(Fault::new(173            "the interlocking crop eats a sheet under two copies on an axis.",174        ));175    }176    let sheet = six::tile_cell(&hex, reps[0], reps[1], crop)?;177    if six::census(&six::skin(&sheet), false).triangles == 0 {178        return Err(Fault::new("that crop leaves no triangle to draw."));179    }180    Ok((hex, sheet))181}182183/// Renders the hexagonal projection of the design tessellated into an interlocking sheet of copies, as SVG.184#[wasm_bindgen]185pub fn tile_svg(186    code: &str,187    number: usize,188    level: usize,189    base: usize,190    projection: &str,191    wide: u32,192    high: u32,193    crop: bool,194    scale: usize,195) -> Result<String, Fault> {196    let reps = reps_of(&[wide, high], 2)?;197    let (_, sheet) = hex_sheet(code, number, level, base, projection, &reps, crop)?;198    Ok(six::svg(&six::framed(&sheet)?, scale, None, 0)?)199}200201// CENSUS202203fn readings(204    fills: u128,205    voids: u128,206    exposed: u128,207    tile_fills: u128,208    tile_exposed: u128,209    copies: u128,210) -> mrlyrs::core::Json {211    json!({212        "copies": copies.to_string(),213        "fills": fills.to_string(),214        "voids": voids.to_string(),215        "exposed": exposed.to_string(),216        "tile_fills": tile_fills.to_string(),217        "tile_exposed": tile_exposed.to_string(),218        "buried": (copies * tile_exposed - exposed).to_string(),219        "ratio": fills as f64 / (fills + voids).max(1) as f64,220    })221}222223fn plane_census(224    code: &str,225    number: usize,226    level: usize,227    base: usize,228    reps: &[usize],229) -> Result<mrlyrs::core::Json, Fault> {230    let (cell, sheet) = plane(code, number, level, base, reps)?;231    let copies = (reps[0] * reps[1]) as u128;232    let fills = two::census::fills(&sheet) as u128;233    let voids = two::census::voids(&sheet) as u128;234    let exposed = two::census::perimeter(&sheet);235    let mut out = readings(236        fills,237        voids,238        exposed,239        two::census::fills(&cell) as u128,240        two::census::perimeter(&cell),241        copies,242    );243    out["tile"] = json!([cell.width(), cell.height()]);244    out["sheet"] = json!([sheet.width(), sheet.height()]);245    out["cells"] = json!((sheet.width() * sheet.height()).to_string());246    if sheet.width() * sheet.height() <= WALK_CELLS {247        let tally = two::census::census(&sheet)?;248        out["vertices"] = json!(tally.vertices);249        out["edges"] = json!(tally.edges);250        out["euler"] = json!(tally.euler);251        out["walked"] = json!(true);252    }253    Ok(out)254}255256fn solid_census(257    code: &str,258    number: usize,259    level: usize,260    base: usize,261    reps: &[usize],262) -> Result<mrlyrs::core::Json, Fault> {263    let (cell, sheet) = solid(code, number, level, base, reps)?;264    let copies = (reps[0] * reps[1] * reps[2]) as u128;265    let fills = three::census::fills(&sheet) as u128;266    let voids = three::census::voids(&sheet) as u128;267    let exposed = three::census::surface(&sheet);268    let mut out = readings(269        fills,270        voids,271        exposed,272        three::census::fills(&cell) as u128,273        three::census::surface(&cell),274        copies,275    );276    out["tile"] = json!([cell.width(), cell.height(), cell.depth()]);277    out["sheet"] = json!([sheet.width(), sheet.height(), sheet.depth()]);278    out["cells"] = json!((sheet.width() * sheet.height() * sheet.depth()).to_string());279    if sheet.width() * sheet.height() * sheet.depth() <= WALK_CELLS {280        let tally = three::census::census(&sheet)?;281        out["vertices"] = json!(tally.vertices);282        out["edges"] = json!(tally.edges);283        out["faces"] = json!(tally.faces);284        out["euler"] = json!(tally.euler);285        out["walked"] = json!(true);286    }287    Ok(out)288}289290fn hex_census(291    code: &str,292    number: usize,293    level: usize,294    base: usize,295    projection: &str,296    reps: &[usize],297    crop: bool,298) -> Result<mrlyrs::core::Json, Fault> {299    let (hex, sheet) = hex_sheet(code, number, level, base, projection, reps, crop)?;300    let (hex, sheet) = (six::skin(&hex), six::skin(&sheet));301    let one = six::census(&hex, false);302    let tally = six::census(&sheet, false);303    let rim = six::census::fills_only(&sheet);304    let mut out = readings(305        tally.fills as u128,306        tally.voids as u128,307        rim.boundary_edges as u128,308        one.fills as u128,309        six::census::fills_only(&hex).boundary_edges as u128,310        (reps[0] * reps[1]) as u128,311    );312    out["tile"] = json!([hex.width(), hex.height()]);313    out["sheet"] = json!([sheet.width(), sheet.height()]);314    out["cells"] = json!(tally.triangles.to_string());315    out["triangles"] = json!(tally.triangles);316    out["boundary"] = json!(tally.boundary_edges);317    out["projection"] = json!(projection);318    if tally.triangles <= WALK_TRIANGLES {319        out["vertices"] = json!(tally.vertices);320        out["edges"] = json!(tally.edges);321        out["euler"] = json!(tally.euler);322        out["walked"] = json!(true);323    }324    Ok(out)325}326327/// Tallies a tiled design in the plane, the cube or the hexagon: the tile and sheet shapes, the copies, the fills, the voids, the exposed faces of the sheet and of one copy, and the faces the tiling buries, as JSON.328///329/// The exposed count is the perimeter in the plane, the surface in the cube and the boundary edges330/// of the filled sub-mesh on the hexagon. Corners, edges and the Euler number ride along under the331/// walk budget, flagged by `walked`.332#[wasm_bindgen]333pub fn tile_census(334    code: &str,335    number: usize,336    level: usize,337    base: usize,338    dimension: usize,339    projection: &str,340    reps: Vec<u32>,341    crop: bool,342) -> Result<String, Fault> {343    let mut out = match dimension {344        2 => plane_census(code, number, level, base, &reps_of(&reps, 2)?)?,345        3 => solid_census(code, number, level, base, &reps_of(&reps, 3)?)?,346        6 => hex_census(347            code,348            number,349            level,350            base,351            projection,352            &reps_of(&reps, 2)?,353            crop,354        )?,355        _ => return Err(Fault::new("a design tiles in dimension 2, 3 or 6.")),356    };357    out["dimension"] = json!(dimension);358    out["side"] = json!(side_of(number, level)?);359    out["reps"] = json!(reps.iter().map(|&r| r as usize).collect::<Vec<usize>>());360    out["crop"] = json!(crop);361    Ok(out.to_string())362}