magic.rs
18.2 kB · rust · 579 lines
1use crate::space::Pack;2use crate::{checked, Fault, Grid};3use mrlyrs::core::{json, Json, Tensor};4use mrlyrs::gen::recipe::{Group, Source, Tile};5use mrlyrs::math::bang::{magic, word, MagicLayer};6use mrlyrs::math::name::{Bang, Named, Word};7use mrlyrs::math::press;8use mrlyrs::math::six::{self, Cell6d};9use mrlyrs::math::three::{quads, Cell3d};10use mrlyrs::math::two::Cell2d;11use wasm_bindgen::prelude::*;1213const PLANE_SIDE: usize = 243;14const SOLID_SIDE: usize = 128;15const HEX_SIDE: usize = 81;16const DRAWN_CELLS: u128 = 1 << 20;1718fn letters(19 codes: Vec<String>,20 numbers: Vec<u32>,21 dimension: usize,22 bases: Vec<u32>,23) -> Result<Vec<MagicLayer>, Fault> {24 if codes.len() != numbers.len() || codes.len() != bases.len() {25 return Err(Fault::new("a word wants one side and one base per letter."));26 }27 if codes.len() < 2 {28 return Err(Fault::new("a word needs at least two letters."));29 }30 let mut out = Vec::with_capacity(codes.len());31 for ((code, number), base) in codes.iter().zip(&numbers).zip(&bases) {32 let base = *base as usize;33 let number = *number as usize;34 if number < 2 {35 return Err(Fault::new(format!("letter side {number} is below two.")));36 }37 let code = checked(code, dimension, base)?;38 out.push(MagicLayer::new(Bang::new(code, dimension, base), number));39 }40 Ok(out)41}4243fn side_of(layers: &[MagicLayer]) -> Result<usize, Fault> {44 let side = word::side(layers)?;45 usize::try_from(side).map_err(|_| Fault::new(format!("side {side} is past what a page draws.")))46}4748fn fits(layers: &[MagicLayer], budget: usize) -> Result<usize, Fault> {49 let side = side_of(layers)?;50 if side > budget {51 return Err(Fault::new(format!(52 "side {side} is more than the {budget} this page draws; drop a letter or use a prefix."53 )));54 }55 Ok(side)56}5758fn drawn(layers: &[MagicLayer], budget: usize) -> Result<Tensor, Fault> {59 fits(layers, budget)?;60 Ok(magic(layers)?)61}6263// PLANE6465/// Builds the plane word as a byte grid, one byte per site.66#[wasm_bindgen]67pub fn magic_grid(codes: Vec<String>, numbers: Vec<u32>, bases: Vec<u32>) -> Result<Grid, Fault> {68 let tile = drawn(&letters(codes, numbers, 2, bases)?, PLANE_SIDE)?;69 Ok(Grid {70 width: tile.shape[1] as u32,71 height: tile.shape[0] as u32,72 types: tile.bytes()?.to_vec(),73 })74}7576// SOLID7778/// Packs the exposed faces of the solid word: two section lengths, then six floats per vertex,79/// position and normal, in the unit box.80#[wasm_bindgen]81pub fn magic_faces(82 codes: Vec<String>,83 numbers: Vec<u32>,84 bases: Vec<u32>,85) -> Result<Vec<f32>, Fault> {86 let tile = drawn(&letters(codes, numbers, 3, bases)?, SOLID_SIDE)?;87 let mut pack = Pack::new();88 for quad in quads(&Cell3d::new(tile)?) {89 pack.quad(quad.verts, quad.normal);90 }91 Ok(pack.buffer())92}9394/// Lists the filled sites of the solid word as x, y, z triples.95#[wasm_bindgen]96pub fn magic_cells(97 codes: Vec<String>,98 numbers: Vec<u32>,99 bases: Vec<u32>,100) -> Result<Vec<u32>, Fault> {101 let tile = drawn(&letters(codes, numbers, 3, bases)?, SOLID_SIDE)?;102 let (cols, deep) = (tile.shape[1], tile.shape[2]);103 let mut out = Vec::new();104 for (flat, &site) in tile.bytes()?.iter().enumerate() {105 if site != 0 {106 out.extend([107 (flat / (cols * deep)) as u32,108 (flat / deep % cols) as u32,109 (flat % deep) as u32,110 ]);111 }112 }113 Ok(out)114}115116/// Counts the exposed faces of the solid word, as a decimal string.117#[wasm_bindgen]118pub fn magic_surface(119 codes: Vec<String>,120 numbers: Vec<u32>,121 bases: Vec<u32>,122) -> Result<String, Fault> {123 let tile = drawn(&letters(codes, numbers, 3, bases)?, SOLID_SIDE)?;124 let (rows, cols, deep) = (tile.shape[0], tile.shape[1], tile.shape[2]);125 let bytes = tile.bytes()?;126 let mut faces = 0u128;127 for i in 0..rows {128 for j in 0..cols {129 for k in 0..deep {130 if bytes[(i * cols + j) * deep + k] == 0 {131 continue;132 }133 faces += 6;134 if i > 0 && bytes[((i - 1) * cols + j) * deep + k] != 0 {135 faces -= 2;136 }137 if j > 0 && bytes[(i * cols + j - 1) * deep + k] != 0 {138 faces -= 2;139 }140 if k > 0 && bytes[(i * cols + j) * deep + k - 1] != 0 {141 faces -= 2;142 }143 }144 }145 }146 Ok(faces.to_string())147}148149/// Counts the exposed edges of the plane word, the perimeter of its filled sites, as a decimal string.150#[wasm_bindgen]151pub fn magic_perimeter(152 codes: Vec<String>,153 numbers: Vec<u32>,154 bases: Vec<u32>,155) -> Result<String, Fault> {156 let tile = drawn(&letters(codes, numbers, 2, bases)?, PLANE_SIDE)?;157 Ok(mrlyrs::math::two::census::perimeter(&Cell2d::new(tile)?).to_string())158}159160// HEXAGON161162fn hexed(163 codes: Vec<String>,164 numbers: Vec<u32>,165 bases: Vec<u32>,166 projection: &str,167) -> Result<Cell6d, Fault> {168 let tile = drawn(&letters(codes, numbers, 3, bases)?, HEX_SIDE)?;169 let cell = Cell3d::new(tile)?;170 Ok(match projection {171 "pro" => six::pro(&cell)?,172 "cut" => six::cut(&cell)?,173 _ => six::iso(&cell)?,174 })175}176177/// Renders the hexagonal projection of the solid word, iso, pro or cut, as SVG at the scale.178#[wasm_bindgen]179pub fn magic_hex(180 codes: Vec<String>,181 numbers: Vec<u32>,182 bases: Vec<u32>,183 projection: &str,184 scale: usize,185) -> Result<String, Fault> {186 Ok(six::svg(187 &hexed(codes, numbers, bases, projection)?,188 scale,189 None,190 0,191 )?)192}193194/// Tallies the hexagonal projection of the solid word: its side, its mesh, its fill and the boundary edges of that fill, as JSON.195#[wasm_bindgen]196pub fn magic_hex_census(197 codes: Vec<String>,198 numbers: Vec<u32>,199 bases: Vec<u32>,200 projection: &str,201) -> Result<String, Fault> {202 let cell = six::skin(&hexed(codes, numbers, bases, projection)?);203 let tally = six::census(&cell, false);204 let rim = six::census::fills_only(&cell);205 Ok(json!({206 "projection": projection,207 "grid": [cell.width(), cell.height()],208 "triangles": tally.triangles,209 "fills": tally.fills,210 "voids": tally.voids,211 "boundary": tally.boundary_edges,212 "edges": tally.edges,213 "vertices": tally.vertices,214 "euler": tally.euler,215 "exposed": rim.boundary_edges,216 "ratio": tally.fills as f64 / tally.triangles.max(1) as f64,217 })218 .to_string())219}220221// CENSUS222223fn pieces(tile: &Tensor) -> u128 {224 let (rows, cols) = (tile.shape[0], tile.shape[1]);225 let mut seen = vec![false; rows * cols];226 let mut count = 0u128;227 let mut stack: Vec<usize> = Vec::new();228 for start in 0..rows * cols {229 if tile.at(start) == 0 || seen[start] {230 continue;231 }232 count += 1;233 seen[start] = true;234 stack.push(start);235 while let Some(at) = stack.pop() {236 let (r, c) = (at / cols, at % cols);237 let mut steps: Vec<usize> = Vec::new();238 if r > 0 {239 steps.push(at - cols);240 }241 if r + 1 < rows {242 steps.push(at + cols);243 }244 if c > 0 {245 steps.push(at - 1);246 }247 if c + 1 < cols {248 steps.push(at + 1);249 }250 for next in steps {251 if tile.at(next) != 0 && !seen[next] {252 seen[next] = true;253 stack.push(next);254 }255 }256 }257 }258 count259}260261fn recipe(layers: &[MagicLayer]) -> Tile {262 let mut tile = Tile::new(Group::Magic);263 tile.sources = layers264 .iter()265 .map(|layer| Source::Code(layer.design.code))266 .collect();267 tile.numbers = layers.iter().map(|layer| layer.number).collect();268 tile.levels = vec![1; layers.len()];269 tile.rotations = vec![0; layers.len()];270 tile.anti = vec![false; layers.len()];271 tile.resize();272 tile273}274275fn count_pieces(layers: &[MagicLayer], dimension: usize) -> (Option<u128>, &'static str) {276 if let Ok(closed) = word::components(layers) {277 return (Some(closed), "closed");278 }279 if dimension != 2 {280 return (None, "");281 }282 let cells = word::side(layers)283 .ok()284 .and_then(|side| side.checked_mul(side));285 match cells {286 Some(total) if total <= DRAWN_CELLS => match magic(layers) {287 Ok(tile) => (Some(pieces(&tile)), "drawn"),288 Err(_) => (None, ""),289 },290 _ => (None, ""),291 }292}293294/// Tallies a word: side, cells, fill, voids, density, dimension, components, the letter list,295/// and the constant, periodic and composite flags, as JSON.296///297/// Every count but the component one is a product over the letters, so the census answers at any298/// length even where the raster is capped.299#[wasm_bindgen]300pub fn magic_census(301 codes: Vec<String>,302 numbers: Vec<u32>,303 dimension: usize,304 bases: Vec<u32>,305) -> Result<String, Fault> {306 let layers = letters(codes, numbers, dimension, bases)?;307 let side = word::side(&layers)?;308 let fill = word::fill(&layers)?;309 let fills = word::fills(&layers)?;310 let cells = side311 .checked_pow(dimension as u32)312 .ok_or_else(|| Fault::new("that word holds more cells than a u128 counts."))?;313 let period = word::period(&layers);314 let native = word::native(&layers);315 let uniform_base = layers316 .iter()317 .all(|l| l.design.base == layers[0].design.base);318 let (components, route) = count_pieces(&layers, dimension);319 let list: Vec<Json> = layers320 .iter()321 .zip(&fills)322 .map(|(layer, count)| {323 Ok(json!({324 "code": layer.design.code.to_string(),325 "number": layer.number,326 "base": layer.design.base,327 "name": Bang::new(layer.design.code, dimension, layer.design.base).to_mrly()?,328 "fill": count.to_string(),329 "cells": (layer.number as u128).pow(dimension as u32).to_string(),330 "dimension": (*count as f64).ln() / (layer.number as f64).ln(),331 "native": layer.number == layer.design.base,332 }))333 })334 .collect::<Result<Vec<Json>, Fault>>()?;335 Ok(json!({336 "length": layers.len(),337 "side": side.to_string(),338 "cells": cells.to_string(),339 "fill": fill.to_string(),340 "voids": (cells - fill).to_string(),341 "ratio": fill as f64 / cells as f64,342 "dimension": word::dimension(&layers)?,343 "period": period,344 "constant": recipe(&layers).degenerate() && uniform_base,345 "periodic": period < layers.len(),346 "native": native,347 "composite": period < layers.len() && native,348 "residue_base": if native {349 layers.iter().map(|l| l.design.base).product::<usize>().to_string()350 } else {351 String::new()352 },353 "components": components.map(|count| count.to_string()).unwrap_or_default(),354 "counted": route,355 "letters": list,356 })357 .to_string())358}359360/// Returns the longest prefix of the sides whose product still fits the budget, at least one.361///362/// A prefix render is the box cover of the whole word at that scale, never a shallower word.363#[wasm_bindgen]364pub fn magic_cap(numbers: Vec<u32>, dimension: usize, budget: usize) -> Result<usize, Fault> {365 if !(2..=3).contains(&dimension) {366 return Err(Fault::new("a word draws in the plane or in the cube."));367 }368 let mut side = 1usize;369 let mut taken = 0usize;370 for number in numbers {371 match side.checked_mul(number as usize) {372 Some(next) if next <= budget => {373 side = next;374 taken += 1;375 }376 _ => break,377 }378 }379 Ok(taken.max(1))380}381382// PRESS383384/// Counts the members of a word's design from its letter fills, without enumeration.385#[wasm_bindgen]386pub fn word_count(387 codes: Vec<String>,388 numbers: Vec<u32>,389 dimension: usize,390 bases: Vec<u32>,391) -> Result<String, Fault> {392 Ok(press::word_count(&letters(codes, numbers, dimension, bases)?)?.to_string())393}394395/// Lists every member of a word's design in ascending order, each as a decimal string.396#[wasm_bindgen]397pub fn word_members(398 codes: Vec<String>,399 numbers: Vec<u32>,400 dimension: usize,401 bases: Vec<u32>,402) -> Result<Vec<String>, Fault> {403 let layers = letters(codes, numbers, dimension, bases)?;404 let count = press::word_count(&layers)?;405 if count > 4096 {406 return Err(Fault::new(format!(407 "{count} members is more than this page lists; shorten the word."408 )));409 }410 Ok(press::word_members(&layers)?411 .iter()412 .map(|m| m.to_string())413 .collect())414}415416/// Returns whether the number lies in the word's design, read in the word's mixed radix.417#[wasm_bindgen]418pub fn word_member(419 codes: Vec<String>,420 numbers: Vec<u32>,421 dimension: usize,422 bases: Vec<u32>,423 number: &str,424) -> Result<bool, Fault> {425 let layers = letters(codes, numbers, dimension, bases)?;426 let value = number427 .trim()428 .parse()429 .map_err(|_| Fault::new(format!("number {number:?} is not a whole number.")))?;430 Ok(press::word_member(&layers, value)?)431}432433/// Returns the diagonal profile of a word by the substitution product, each count a decimal string.434#[wasm_bindgen]435pub fn word_profile(436 codes: Vec<String>,437 numbers: Vec<u32>,438 dimension: usize,439 bases: Vec<u32>,440) -> Result<Vec<String>, Fault> {441 let layers = letters(codes, numbers, dimension, bases)?;442 let side = word::side(&layers)?;443 let heights = (dimension as u128) * (side - 1) + 1;444 if heights > 100_000 {445 return Err(Fault::new(format!(446 "{heights} diagonal heights is more than this page reads; shorten the word."447 )));448 }449 Ok(press::word_profile(&layers)?450 .iter()451 .map(|count| count.to_string())452 .collect())453}454455// NAMES456457fn spelt(458 codes: Vec<String>,459 numbers: Vec<u32>,460 bases: Vec<u32>,461 dimension: usize,462) -> Result<Word, Fault> {463 let layers = letters(codes, numbers, dimension, bases)?;464 Ok(Word {465 kind: mrlyrs::math::name::word::Kind,466 dim: dimension,467 magic: layers.iter().map(|layer| layer.design.code).collect(),468 side: layers.iter().map(|layer| layer.number).collect(),469 base: Some(layers.iter().map(|layer| layer.design.base).collect()),470 }471 .checked()?)472}473474/// Prints the name of a word as a line of prose.475#[wasm_bindgen]476pub fn magic_name(477 codes: Vec<String>,478 numbers: Vec<u32>,479 bases: Vec<u32>,480 dimension: usize,481) -> Result<String, Fault> {482 Ok(spelt(codes, numbers, bases, dimension)?.to_mrly()?)483}484485/// Prints the file name of a word, the form a query string carries.486#[wasm_bindgen]487pub fn magic_key(488 codes: Vec<String>,489 numbers: Vec<u32>,490 bases: Vec<u32>,491 dimension: usize,492) -> Result<String, Fault> {493 Ok(spelt(codes, numbers, bases, dimension)?.to_file()?)494}495496/// Reads a word's file name back into its dim, codes, sides and bases, as JSON.497#[wasm_bindgen]498pub fn magic_parse(text: &str) -> Result<String, Fault> {499 let word = Word::from_file(text)?;500 Ok(json!({501 "dim": word.dim,502 "codes": word.magic.iter().map(u128::to_string).collect::<Vec<String>>(),503 "numbers": word.side,504 "bases": word.bases(),505 })506 .to_string())507}508509// RATES510511/// Charts the prefix rates of a schedule over the word's first two letters, in log two units.512///513/// It returns the component rate and the fill rate at every prefix length, the same pair along the514/// periodic control at the same letter frequencies, the constant-word functional the schedule515/// predicts, and the interior-frequency exponent the fill law gives.516#[wasm_bindgen]517pub fn magic_rates(518 codes: Vec<String>,519 numbers: Vec<u32>,520 bases: Vec<u32>,521 schedule: &str,522 length: usize,523) -> Result<String, Fault> {524 let layers = letters(codes, numbers, 2, bases)?;525 let kind = schedule.parse::<word::Schedule>()?;526 let pair = (layers[0].clone(), layers[1].clone());527 let spelt = word::spell(kind, pair.clone(), length.clamp(2, 120));528 let control = word::spell(word::Schedule::Periodic, pair.clone(), length.clamp(2, 120));529 let mut rows = word::rates(&spelt)?;530 let mut mirror = word::rates(&control)?;531 let take = rows.len().min(mirror.len());532 rows.truncate(take);533 mirror.truncate(take);534 let fills = word::fills(&[pair.0.clone(), pair.1.clone()])?;535 let (first, second) = kind.frequencies();536 let limit = first * (fills[0] as f64).log2() + second * (fills[1] as f64).log2();537 let alphabet = [&pair.0, &pair.1].iter().all(|letter| {538 letter.number == 2 && letter.design.base == 2 && (1..=15).contains(&letter.design.code)539 });540 Ok(json!({541 "schedule": schedule,542 "length": rows.len(),543 "letters": [544 Bang::new(pair.0.design.code, 2, pair.0.design.base).to_mrly()?,545 Bang::new(pair.1.design.code, 2, pair.1.design.base).to_mrly()?,546 ],547 "rows": rows.iter().map(|(a, b)| vec![*a, *b]).collect::<Vec<Vec<f64>>>(),548 "control": mirror.iter().map(|(a, _)| *a).collect::<Vec<f64>>(),549 "phi": word::constant_functional(&spelt)?,550 "limit": limit,551 "alphabet": alphabet,552 })553 .to_string())554}555556/// Reads the carpet staircase to the depth: its letters, its length and its dimension at every557/// block, beside the flat dimension of the constant word its first letter spells.558#[wasm_bindgen]559pub fn magic_staircase(depth: usize) -> Result<String, Fault> {560 if !(1..=8).contains(&depth) {561 return Err(Fault::new("the staircase runs from one block to eight."));562 }563 let mut rows = Vec::new();564 for step in 1..=depth {565 let block = word::staircase(step)?;566 rows.push(json!({567 "blocks": step,568 "length": block.len(),569 "dimension": word::dimension(&block)?,570 "sides": block.iter().map(|layer| layer.number).collect::<Vec<usize>>(),571 }));572 }573 let one = word::staircase(1)?;574 Ok(json!({575 "rows": rows,576 "constant": word::dimension(&[one[0].clone(), one[0].clone()])?,577 })578 .to_string())579}