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}