radix.rs

8.5 kB · rust · 265 lines

1use crate::Fault;2use mrlycore::json;3use mrlynum::gauss::Ring;4use mrlynum::radix::{self, Base, Radix};5use wasm_bindgen::prelude::*;67/// The drawing budget: the most points one level may carry.8pub const POINTS: usize = 1 << 16;910/// The deepest level offered, whatever the digit count.11pub const DEEPEST: usize = 16;1213/// The carpet code: the nine box cells of the side-three tile with the centre out.14pub const CARPET: u128 = 0b111101111;1516fn named(name: &str) -> Result<Ring, Fault> {17    Ring::named(name)18        .ok_or_else(|| Fault::new(format!("ring {name:?} is not a ring of this plane.")))19}2021fn whole(text: &str) -> Result<i64, Fault> {22    text.trim()23        .parse()24        .map_err(|_| Fault::new(format!("{text:?} is not a whole number.")))25}2627fn pairs(text: &str) -> Result<Vec<(i64, i64)>, Fault> {28    text.split('_')29        .filter(|part| !part.trim().is_empty())30        .map(|part| {31            let (a, c) = part32                .split_once(':')33                .ok_or_else(|| Fault::new(format!("digit {part:?} is not a pair a:c.")))?;34            Ok((whole(a)?, whole(c)?))35        })36        .collect()37}3839fn indices(text: &str) -> Result<Vec<usize>, Fault> {40    text.split('_')41        .filter(|part| !part.trim().is_empty())42        .map(|part| {43            part.trim()44                .parse()45                .map_err(|_| Fault::new(format!("twist {part:?} is not a unit index.")))46        })47        .collect()48}4950fn ceiling(size: usize) -> usize {51    let mut level = 1;52    while level < DEEPEST53        && size54            .checked_pow(level as u32 + 1)55            .is_some_and(|count| count <= POINTS)56    {57        level += 1;58    }59    level60}6162fn design(ring: &str, a: i32, c: i32, digits: &str, twists: &str) -> Result<Radix, Fault> {63    let ring = named(ring)?;64    let (a, c) = (i64::from(a), i64::from(c));65    if ring.norm(a, c) < 2 {66        return Err(Fault::new(format!(67            "the base {a}, {c} has norm {}, below two.",68            ring.norm(a, c)69        )));70    }71    let digits = pairs(digits)?;72    if digits.is_empty() {73        return Err(Fault::new("a design needs a digit."));74    }75    let units = ring.associates(1, 0);76    let picked = indices(twists)?;77    if picked.len() != digits.len() {78        return Err(Fault::new(format!(79            "{} digits carry {} twists.",80            digits.len(),81            picked.len()82        )));83    }84    if let Some(&over) = picked.iter().find(|&&i| i >= units.len()) {85        return Err(Fault::new(format!(86            "unit {over} is past the {} units of the ring.",87            units.len()88        )));89    }90    let turns = picked.into_iter().map(|i| units[i]).collect();91    let base = Base::new(ring, (a, c));92    for (i, &z) in digits.iter().enumerate() {93        for &w in &digits[..i] {94            if base.congruent(z, w) {95                return Err(Fault::new(format!(96                    "the digits {}:{} and {}:{} are congruent modulo the base.",97                    w.0, w.1, z.0, z.198                )));99            }100        }101    }102    Ok(Radix::new(base, digits, turns))103}104105fn levelled(radix: &Radix, level: usize) -> Result<usize, Fault> {106    let cap = ceiling(radix.size());107    if level < 1 || level > cap {108        return Err(Fault::new(format!(109            "level {level} is past the cap of {cap} for {} digits at the budget of {POINTS} points.",110            radix.size()111        )));112    }113    Ok(cap)114}115116fn spelling(radix: &Radix) -> (String, String) {117    let units = radix.ring().associates(1, 0);118    let digits = radix119        .digits()120        .iter()121        .map(|(a, c)| format!("{a}:{c}"))122        .collect::<Vec<String>>()123        .join("_");124    let twists = radix125        .twists()126        .iter()127        .map(|u| {128            units129                .iter()130                .position(|v| v == u)131                .expect("a twist is a unit")132                .to_string()133        })134        .collect::<Vec<String>>()135        .join("_");136    (digits, twists)137}138139fn word(ring: Ring) -> &'static str {140    match ring {141        Ring::Gaussian => "gaussian",142        Ring::Eisenstein => "eisenstein",143    }144}145146fn card(name: &str, label: &str, radix: &Radix, level: usize, line: bool) -> mrlycore::Json {147    let (digits, twists) = spelling(radix);148    let base = radix.base().value();149    json!({150        "name": name,151        "label": label,152        "ring": word(radix.ring()),153        "a": base.0,154        "c": base.1,155        "digits": digits,156        "twists": twists,157        "level": level,158        "line": line,159    })160}161162/// Returns the dial itself: the rings, the bases offered on each, the units of each ring and the presets, as JSON.163///164/// Every preset is a quintuple built by `mrlynum::radix`, spelled back as the digit list and the unit indices the page carries in its query.165#[wasm_bindgen]166pub fn radix_menu() -> String {167    let places = |ring: Ring, list: &[(i64, i64)]| {168        list.iter()169            .map(|&(a, c)| json!({"a": a, "c": c, "norm": ring.norm(a, c)}))170            .collect::<Vec<mrlycore::Json>>()171    };172    let bases = |ring: Ring, list: Vec<(i64, i64)>| {173        json!({174            "name": word(ring),175            "units": ring.associates(1, 0).iter().map(|&(a, c)| json!({"a": a, "c": c})).collect::<Vec<mrlycore::Json>>(),176            "bases": places(ring, &list),177        })178    };179    json!({180        "rings": [181            bases(Ring::Gaussian, vec![(2, 0), (1, 1), (2, 1), (3, 0)]),182            bases(Ring::Eisenstein, vec![(2, 0), (2, 1), (3, 0), (3, 1)]),183        ],184        "presets": [185            card("koch", "the Koch curve", &radix::koch(), 5, true),186            card("gasket", "the gasket", &radix::gasket(), 7, false),187            card("twindragon", "the twindragon", &radix::twindragon(), 14, false),188            card("tile7", "the norm-7 tile", &radix::flowsnake(), 5, false),189            card("carpet", "the carpet, box digits", &radix::tile(3, CARPET), 5, false),190        ],191        "points": POINTS,192    })193    .to_string()194}195196/// Returns the deepest level a digit list is drawn at: the largest `L` with `(card F)^L` inside the budget.197#[wasm_bindgen]198pub fn radix_cap(digits: &str) -> Result<usize, Fault> {199    let digits = pairs(digits)?;200    if digits.is_empty() {201        return Err(Fault::new("a design needs a digit."));202    }203    Ok(ceiling(digits.len()))204}205206/// Reads a radix design: its base, the canonical residues, the digits and their classes, the code of those classes, the fill `(card F)^L`, the distinct points, the similarity dimension and whether every digit is canonical, as JSON.207///208/// The digits are spelled `a:c` and joined by `_`, the twists are the indices of the ring's units in turning order from one, one per digit.209#[wasm_bindgen]210pub fn radix_read(211    ring: &str,212    a: i32,213    c: i32,214    digits: &str,215    twists: &str,216    level: usize,217) -> Result<String, Fault> {218    let radix = design(ring, a, c, digits, twists)?;219    let cap = levelled(&radix, level)?;220    let base = radix.base();221    let residues = base.residues();222    let units = radix.ring().associates(1, 0);223    let fill = radix.fill(level);224    let distinct = radix.distinct(level);225    Ok(json!({226        "ring": word(radix.ring()),227        "a": base.value().0,228        "c": base.value().1,229        "q": base.norm(),230        "level": level,231        "cap": cap,232        "size": radix.size(),233        "fill": fill.to_string(),234        "distinct": distinct,235        "glued": (distinct as u128) < fill,236        "dimension": radix.dimension(),237        "canonical": radix.canonical(),238        "code": radix.code().to_string(),239        "residues": residues.iter().map(|&(x, y)| json!({"a": x, "c": y})).collect::<Vec<mrlycore::Json>>(),240        "digits": radix.digits().iter().map(|&(x, y)| json!({"a": x, "c": y, "class": base.class((x, y))})).collect::<Vec<mrlycore::Json>>(),241        "twists": radix.twists().iter().map(|u| units.iter().position(|v| v == u).expect("a twist is a unit")).collect::<Vec<usize>>(),242        "units": units.iter().map(|&(x, y)| json!({"a": x, "c": y})).collect::<Vec<mrlycore::Json>>(),243    })244    .to_string())245}246247/// Returns the level-`L` points of a radix design in the plane as `x, y` pairs of floats, one pair a word, in digit-lexicographic order.248#[wasm_bindgen]249pub fn radix_points(250    ring: &str,251    a: i32,252    c: i32,253    digits: &str,254    twists: &str,255    level: usize,256) -> Result<Vec<f32>, Fault> {257    let radix = design(ring, a, c, digits, twists)?;258    levelled(&radix, level)?;259    let mut out = Vec::with_capacity(2 * radix.fill(level) as usize);260    for (x, y) in radix.plane(level) {261        out.push(x as f32);262        out.push(y as f32);263    }264    Ok(out)265}