magic.rs
18.7 kB · rust · 598 lines
1use crate::{checked, Fault, Grid, Pixels};2use mrlycore::cell::mapping;3use mrlycore::tile::{Group, Source, Tile};4use mrlycore::{json, Json, Mode, Tensor};5use mrlylab::press;6use mrlymath::bang::{magic, word, MagicLayer};7use mrlymath::name::{Bang, Named, Word};8use mrlymath::six::{self, Cell6d};9use mrlymath::space::Pack;10use mrlymath::three::{quads, Cell3d};11use mrlymath::two::Cell2d;12use wasm_bindgen::prelude::*;1314const PLANE_SIDE: usize = 243;15const SOLID_SIDE: usize = 128;16const HEX_SIDE: usize = 81;17const DRAWN_CELLS: u128 = 1 << 20;1819fn letters(20 codes: Vec<String>,21 numbers: Vec<u32>,22 dimension: usize,23 bases: Vec<u32>,24) -> Result<Vec<MagicLayer>, Fault> {25 if codes.len() != numbers.len() || codes.len() != bases.len() {26 return Err(Fault::new("a word wants one side and one base per letter."));27 }28 if codes.len() < 2 {29 return Err(Fault::new("a word needs at least two letters."));30 }31 let mut out = Vec::with_capacity(codes.len());32 for ((code, number), base) in codes.iter().zip(&numbers).zip(&bases) {33 let base = *base as usize;34 let number = *number as usize;35 if number < 2 {36 return Err(Fault::new(format!("letter side {number} is below two.")));37 }38 let code = checked(code, dimension, base)?;39 out.push(MagicLayer::new(Bang::new(code, dimension, base), number));40 }41 Ok(out)42}4344fn side_of(layers: &[MagicLayer]) -> Result<usize, Fault> {45 let side = word::side(layers)?;46 usize::try_from(side).map_err(|_| Fault::new(format!("side {side} is past what a page draws.")))47}4849fn fits(layers: &[MagicLayer], budget: usize) -> Result<usize, Fault> {50 let side = side_of(layers)?;51 if side > budget {52 return Err(Fault::new(format!(53 "side {side} is more than the {budget} this page draws; drop a letter or use a prefix."54 )));55 }56 Ok(side)57}5859fn drawn(layers: &[MagicLayer], budget: usize) -> Result<Tensor, Fault> {60 fits(layers, budget)?;61 Ok(magic(layers)?)62}6364// PLANE6566/// Builds the plane word as a byte grid, one byte per site.67#[wasm_bindgen]68pub fn magic_grid(codes: Vec<String>, numbers: Vec<u32>, bases: Vec<u32>) -> Result<Grid, Fault> {69 let tile = drawn(&letters(codes, numbers, 2, bases)?, PLANE_SIDE)?;70 Ok(Grid {71 width: tile.shape[1] as u32,72 height: tile.shape[0] as u32,73 types: tile.bytes().to_vec(),74 })75}7677/// Paints the plane word: filled sites black, empty sites white.78#[wasm_bindgen]79pub fn magic_pixels(80 codes: Vec<String>,81 numbers: Vec<u32>,82 bases: Vec<u32>,83) -> Result<Pixels, Fault> {84 let tile = drawn(&letters(codes, numbers, 2, bases)?, PLANE_SIDE)?;85 let cell = Cell2d::new(tile).paint(&mapping(), Mode::Type);86 let (width, height) = (cell.width(), cell.height());87 Ok(Pixels::of(88 width,89 height,90 cell.cell.colors.unwrap_or_default(),91 ))92}9394// SOLID9596/// Packs the exposed faces of the solid word: two section lengths, then six floats per vertex,97/// position and normal, in the unit box.98#[wasm_bindgen]99pub fn magic_faces(100 codes: Vec<String>,101 numbers: Vec<u32>,102 bases: Vec<u32>,103) -> Result<Vec<f32>, Fault> {104 let tile = drawn(&letters(codes, numbers, 3, bases)?, SOLID_SIDE)?;105 let mut pack = Pack::new();106 for quad in quads(&Cell3d::new(tile)) {107 pack.quad(quad.verts, quad.normal);108 }109 Ok(pack.buffer())110}111112/// Lists the filled sites of the solid word as x, y, z triples.113#[wasm_bindgen]114pub fn magic_cells(115 codes: Vec<String>,116 numbers: Vec<u32>,117 bases: Vec<u32>,118) -> Result<Vec<u32>, Fault> {119 let tile = drawn(&letters(codes, numbers, 3, bases)?, SOLID_SIDE)?;120 let (cols, deep) = (tile.shape[1], tile.shape[2]);121 let mut out = Vec::new();122 for (flat, &site) in tile.bytes().iter().enumerate() {123 if site != 0 {124 out.extend([125 (flat / (cols * deep)) as u32,126 (flat / deep % cols) as u32,127 (flat % deep) as u32,128 ]);129 }130 }131 Ok(out)132}133134/// Counts the exposed faces of the solid word, as a decimal string.135#[wasm_bindgen]136pub fn magic_surface(137 codes: Vec<String>,138 numbers: Vec<u32>,139 bases: Vec<u32>,140) -> Result<String, Fault> {141 let tile = drawn(&letters(codes, numbers, 3, bases)?, SOLID_SIDE)?;142 let (rows, cols, deep) = (tile.shape[0], tile.shape[1], tile.shape[2]);143 let bytes = tile.bytes();144 let mut faces = 0u128;145 for i in 0..rows {146 for j in 0..cols {147 for k in 0..deep {148 if bytes[(i * cols + j) * deep + k] == 0 {149 continue;150 }151 faces += 6;152 if i > 0 && bytes[((i - 1) * cols + j) * deep + k] != 0 {153 faces -= 2;154 }155 if j > 0 && bytes[(i * cols + j - 1) * deep + k] != 0 {156 faces -= 2;157 }158 if k > 0 && bytes[(i * cols + j) * deep + k - 1] != 0 {159 faces -= 2;160 }161 }162 }163 }164 Ok(faces.to_string())165}166167/// Counts the exposed edges of the plane word, the perimeter of its filled sites, as a decimal string.168#[wasm_bindgen]169pub fn magic_perimeter(170 codes: Vec<String>,171 numbers: Vec<u32>,172 bases: Vec<u32>,173) -> Result<String, Fault> {174 let tile = drawn(&letters(codes, numbers, 2, bases)?, PLANE_SIDE)?;175 Ok(mrlymath::two::census::perimeter(&Cell2d::new(tile)).to_string())176}177178// HEXAGON179180fn hexed(181 codes: Vec<String>,182 numbers: Vec<u32>,183 bases: Vec<u32>,184 projection: &str,185) -> Result<Cell6d, Fault> {186 let tile = drawn(&letters(codes, numbers, 3, bases)?, HEX_SIDE)?;187 let cell = Cell3d::new(tile);188 Ok(match projection {189 "pro" => six::pro(&cell)?,190 "cut" => six::cut(&cell)?,191 _ => six::iso(&cell)?,192 })193}194195/// Renders the hexagonal projection of the solid word, iso, pro or cut, as SVG at the scale.196#[wasm_bindgen]197pub fn magic_hex(198 codes: Vec<String>,199 numbers: Vec<u32>,200 bases: Vec<u32>,201 projection: &str,202 scale: usize,203) -> Result<String, Fault> {204 Ok(six::svg(205 &hexed(codes, numbers, bases, projection)?,206 scale,207 None,208 0,209 )?)210}211212/// Tallies the hexagonal projection of the solid word: its side, its mesh, its fill and the boundary edges of that fill, as JSON.213#[wasm_bindgen]214pub fn magic_hex_census(215 codes: Vec<String>,216 numbers: Vec<u32>,217 bases: Vec<u32>,218 projection: &str,219) -> Result<String, Fault> {220 let cell = six::skin(&hexed(codes, numbers, bases, projection)?);221 let tally = six::census(&cell, false);222 let rim = six::census::fills_only(&cell);223 Ok(json!({224 "projection": projection,225 "grid": [cell.width(), cell.height()],226 "triangles": tally.triangles,227 "fills": tally.fills,228 "voids": tally.voids,229 "boundary": tally.boundary_edges,230 "edges": tally.edges,231 "vertices": tally.vertices,232 "euler": tally.euler,233 "exposed": rim.boundary_edges,234 "ratio": tally.fills as f64 / tally.triangles.max(1) as f64,235 })236 .to_string())237}238239// CENSUS240241fn pieces(tile: &Tensor) -> u128 {242 let (rows, cols) = (tile.shape[0], tile.shape[1]);243 let bytes = tile.bytes();244 let mut seen = vec![false; rows * cols];245 let mut count = 0u128;246 let mut stack: Vec<usize> = Vec::new();247 for start in 0..rows * cols {248 if bytes[start] == 0 || seen[start] {249 continue;250 }251 count += 1;252 seen[start] = true;253 stack.push(start);254 while let Some(at) = stack.pop() {255 let (r, c) = (at / cols, at % cols);256 let mut steps: Vec<usize> = Vec::new();257 if r > 0 {258 steps.push(at - cols);259 }260 if r + 1 < rows {261 steps.push(at + cols);262 }263 if c > 0 {264 steps.push(at - 1);265 }266 if c + 1 < cols {267 steps.push(at + 1);268 }269 for next in steps {270 if bytes[next] != 0 && !seen[next] {271 seen[next] = true;272 stack.push(next);273 }274 }275 }276 }277 count278}279280fn recipe(layers: &[MagicLayer]) -> Tile {281 let mut tile = Tile::new(Group::Magic);282 tile.sources = layers283 .iter()284 .map(|layer| Source::Code(layer.design.code))285 .collect();286 tile.numbers = layers.iter().map(|layer| layer.number).collect();287 tile.levels = vec![1; layers.len()];288 tile.rotations = vec![0; layers.len()];289 tile.anti = vec![false; layers.len()];290 tile.resize();291 tile292}293294fn count_pieces(layers: &[MagicLayer], dimension: usize) -> (Option<u128>, &'static str) {295 if let Ok(closed) = word::components(layers) {296 return (Some(closed), "closed");297 }298 if dimension != 2 {299 return (None, "");300 }301 let cells = word::side(layers)302 .ok()303 .and_then(|side| side.checked_mul(side));304 match cells {305 Some(total) if total <= DRAWN_CELLS => match magic(layers) {306 Ok(tile) => (Some(pieces(&tile)), "drawn"),307 Err(_) => (None, ""),308 },309 _ => (None, ""),310 }311}312313/// Tallies a word: side, cells, fill, voids, density, dimension, components, the letter list,314/// and the constant, periodic and composite flags, as JSON.315///316/// Every count but the component one is a product over the letters, so the census answers at any317/// length even where the raster is capped.318#[wasm_bindgen]319pub fn magic_census(320 codes: Vec<String>,321 numbers: Vec<u32>,322 dimension: usize,323 bases: Vec<u32>,324) -> Result<String, Fault> {325 let layers = letters(codes, numbers, dimension, bases)?;326 let side = word::side(&layers)?;327 let fill = word::fill(&layers)?;328 let fills = word::fills(&layers)?;329 let cells = side330 .checked_pow(dimension as u32)331 .ok_or_else(|| Fault::new("that word holds more cells than a u128 counts."))?;332 let period = word::period(&layers);333 let native = word::native(&layers);334 let uniform_base = layers335 .iter()336 .all(|l| l.design.base == layers[0].design.base);337 let (components, route) = count_pieces(&layers, dimension);338 let list: Vec<Json> = layers339 .iter()340 .zip(&fills)341 .map(|(layer, count)| {342 json!({343 "code": layer.design.code.to_string(),344 "number": layer.number,345 "base": layer.design.base,346 "name": Bang::new(layer.design.code, dimension, layer.design.base).to_mrly(),347 "fill": count.to_string(),348 "cells": (layer.number as u128).pow(dimension as u32).to_string(),349 "dimension": (*count as f64).ln() / (layer.number as f64).ln(),350 "native": layer.number == layer.design.base,351 })352 })353 .collect();354 Ok(json!({355 "length": layers.len(),356 "side": side.to_string(),357 "cells": cells.to_string(),358 "fill": fill.to_string(),359 "voids": (cells - fill).to_string(),360 "ratio": fill as f64 / cells as f64,361 "dimension": word::dimension(&layers)?,362 "period": period,363 "constant": recipe(&layers).degenerate() && uniform_base,364 "periodic": period < layers.len(),365 "native": native,366 "composite": period < layers.len() && native,367 "residue_base": if native {368 layers.iter().map(|l| l.design.base).product::<usize>().to_string()369 } else {370 String::new()371 },372 "components": components.map(|count| count.to_string()).unwrap_or_default(),373 "counted": route,374 "letters": list,375 })376 .to_string())377}378379/// Returns the longest prefix of the sides whose product still fits the budget, at least one.380///381/// A prefix render is the box cover of the whole word at that scale, never a shallower word.382#[wasm_bindgen]383pub fn magic_cap(numbers: Vec<u32>, dimension: usize, budget: usize) -> Result<usize, Fault> {384 if !(2..=3).contains(&dimension) {385 return Err(Fault::new("a word draws in the plane or in the cube."));386 }387 let mut side = 1usize;388 let mut taken = 0usize;389 for number in numbers {390 match side.checked_mul(number as usize) {391 Some(next) if next <= budget => {392 side = next;393 taken += 1;394 }395 _ => break,396 }397 }398 Ok(taken.max(1))399}400401// PRESS402403/// Counts the members of a word's design from its letter fills, without enumeration.404#[wasm_bindgen]405pub fn word_count(406 codes: Vec<String>,407 numbers: Vec<u32>,408 dimension: usize,409 bases: Vec<u32>,410) -> Result<String, Fault> {411 Ok(press::word_count(&letters(codes, numbers, dimension, bases)?)?.to_string())412}413414/// Lists every member of a word's design in ascending order, each as a decimal string.415#[wasm_bindgen]416pub fn word_members(417 codes: Vec<String>,418 numbers: Vec<u32>,419 dimension: usize,420 bases: Vec<u32>,421) -> Result<Vec<String>, Fault> {422 let layers = letters(codes, numbers, dimension, bases)?;423 let count = press::word_count(&layers)?;424 if count > 4096 {425 return Err(Fault::new(format!(426 "{count} members is more than this page lists; shorten the word."427 )));428 }429 Ok(press::word_members(&layers)?430 .iter()431 .map(|m| m.to_string())432 .collect())433}434435/// Returns whether the number lies in the word's design, read in the word's mixed radix.436#[wasm_bindgen]437pub fn word_member(438 codes: Vec<String>,439 numbers: Vec<u32>,440 dimension: usize,441 bases: Vec<u32>,442 number: &str,443) -> Result<bool, Fault> {444 let layers = letters(codes, numbers, dimension, bases)?;445 let value = number446 .trim()447 .parse()448 .map_err(|_| Fault::new(format!("number {number:?} is not a whole number.")))?;449 Ok(press::word_member(&layers, value)?)450}451452/// Returns the diagonal profile of a word by the substitution product, each count a decimal string.453#[wasm_bindgen]454pub fn word_profile(455 codes: Vec<String>,456 numbers: Vec<u32>,457 dimension: usize,458 bases: Vec<u32>,459) -> Result<Vec<String>, Fault> {460 let layers = letters(codes, numbers, dimension, bases)?;461 let side = word::side(&layers)?;462 let heights = (dimension as u128) * (side - 1) + 1;463 if heights > 100_000 {464 return Err(Fault::new(format!(465 "{heights} diagonal heights is more than this page reads; shorten the word."466 )));467 }468 Ok(press::word_profile(&layers)?469 .iter()470 .map(|count| count.to_string())471 .collect())472}473474// NAMES475476fn spelt(477 codes: Vec<String>,478 numbers: Vec<u32>,479 bases: Vec<u32>,480 dimension: usize,481) -> Result<Word, Fault> {482 let layers = letters(codes, numbers, dimension, bases)?;483 Ok(Word {484 kind: mrlymath::name::word::Kind,485 dim: dimension,486 magic: layers.iter().map(|layer| layer.design.code).collect(),487 side: layers.iter().map(|layer| layer.number).collect(),488 base: Some(layers.iter().map(|layer| layer.design.base).collect()),489 }490 .checked()?)491}492493/// Prints the name of a word as a line of prose.494#[wasm_bindgen]495pub fn magic_name(496 codes: Vec<String>,497 numbers: Vec<u32>,498 bases: Vec<u32>,499 dimension: usize,500) -> Result<String, Fault> {501 Ok(spelt(codes, numbers, bases, dimension)?.to_mrly())502}503504/// Prints the file name of a word, the form a query string carries.505#[wasm_bindgen]506pub fn magic_key(507 codes: Vec<String>,508 numbers: Vec<u32>,509 bases: Vec<u32>,510 dimension: usize,511) -> Result<String, Fault> {512 Ok(spelt(codes, numbers, bases, dimension)?.to_file())513}514515/// Reads a word's file name back into its dim, codes, sides and bases, as JSON.516#[wasm_bindgen]517pub fn magic_parse(text: &str) -> Result<String, Fault> {518 let word = Word::from_file(text)?;519 Ok(json!({520 "dim": word.dim,521 "codes": word.magic.iter().map(u128::to_string).collect::<Vec<String>>(),522 "numbers": word.side,523 "bases": word.bases(),524 })525 .to_string())526}527528// RATES529530/// Charts the prefix rates of a schedule over the word's first two letters, in log two units.531///532/// It returns the component rate and the fill rate at every prefix length, the same pair along the533/// periodic control at the same letter frequencies, the constant-word functional the schedule534/// predicts, and the interior-frequency exponent the fill law gives.535#[wasm_bindgen]536pub fn magic_rates(537 codes: Vec<String>,538 numbers: Vec<u32>,539 bases: Vec<u32>,540 schedule: &str,541 length: usize,542) -> Result<String, Fault> {543 let layers = letters(codes, numbers, 2, bases)?;544 let kind = word::Schedule::parse(schedule)?;545 let pair = (layers[0].clone(), layers[1].clone());546 let spelt = word::spell(kind, pair.clone(), length.clamp(2, 120));547 let control = word::spell(word::Schedule::Periodic, pair.clone(), length.clamp(2, 120));548 let mut rows = word::rates(&spelt)?;549 let mut mirror = word::rates(&control)?;550 let take = rows.len().min(mirror.len());551 rows.truncate(take);552 mirror.truncate(take);553 let fills = word::fills(&[pair.0.clone(), pair.1.clone()])?;554 let (first, second) = kind.frequencies();555 let limit = first * (fills[0] as f64).log2() + second * (fills[1] as f64).log2();556 let alphabet = [&pair.0, &pair.1].iter().all(|letter| {557 letter.number == 2 && letter.design.base == 2 && (1..=15).contains(&letter.design.code)558 });559 Ok(json!({560 "schedule": schedule,561 "length": rows.len(),562 "letters": [563 Bang::new(pair.0.design.code, 2, pair.0.design.base).to_mrly(),564 Bang::new(pair.1.design.code, 2, pair.1.design.base).to_mrly(),565 ],566 "rows": rows.iter().map(|(a, b)| vec![*a, *b]).collect::<Vec<Vec<f64>>>(),567 "control": mirror.iter().map(|(a, _)| *a).collect::<Vec<f64>>(),568 "phi": word::constant_functional(&spelt)?,569 "limit": limit,570 "alphabet": alphabet,571 })572 .to_string())573}574575/// Reads the carpet staircase to the depth: its letters, its length and its dimension at every576/// block, beside the flat dimension of the constant word its first letter spells.577#[wasm_bindgen]578pub fn magic_staircase(depth: usize) -> Result<String, Fault> {579 if !(1..=8).contains(&depth) {580 return Err(Fault::new("the staircase runs from one block to eight."));581 }582 let mut rows = Vec::new();583 for step in 1..=depth {584 let block = word::staircase(step)?;585 rows.push(json!({586 "blocks": step,587 "length": block.len(),588 "dimension": word::dimension(&block)?,589 "sides": block.iter().map(|layer| layer.number).collect::<Vec<usize>>(),590 }));591 }592 let one = word::staircase(1)?;593 Ok(json!({594 "rows": rows,595 "constant": word::dimension(&[one[0].clone(), one[0].clone()])?,596 })597 .to_string())598}