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}