tile.rs

10.6 kB · rust · 361 lines

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