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}