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}