shell.rs

9.2 kB · rust · 236 lines

1use crate::{code_of, rgba, theme, Fault, Pixels};2use mrlyrs::core::json;3use mrlyrs::core::tensor::Tensor;4use mrlyrs::math::bang::factory;5use mrlyrs::math::bang::Code;6use mrlyrs::math::shape::{crossing_tree, Shell};7use wasm_bindgen::prelude::*;89const RADIUS_CAP: u32 = 242;10const SHEET: usize = 486;11const EDGE: usize = 2;12const MARK: usize = 5;1314fn guarded(code: &str, number: usize, base: usize, radius: u32) -> Result<(Shell, usize), Fault> {15    if number < 2 {16        return Err(Fault::new("the side number must be at least 2."));17    }18    if !(1..=RADIUS_CAP).contains(&radius) {19        return Err(Fault::new(format!(20            "the radius must be between 1 and {RADIUS_CAP} cells."21        )));22    }23    let tile = factory::create(Code::from(code_of(code)?), number, 2, base, 1)?;24    let keep: Vec<bool> = tile.bytes()?.iter().map(|&b| b != 0).collect();25    let tree = crossing_tree(radius as u64, number as u64, &keep);26    let depth = tree.levels.len() - 1;27    Ok((tree, depth))28}2930fn body(code: &str, number: usize, base: usize, depth: usize) -> Result<Tensor, Fault> {31    Ok(factory::create(32        Code::from(code_of(code)?),33        number,34        2,35        base,36        depth,37    )?)38}3940fn seated(41    tree: &Shell,42    depth: usize,43    level: Option<u32>,44    index: Option<u32>,45) -> Result<(usize, usize), Fault> {46    let root = level.map_or(depth, |j| j as usize);47    if root > depth {48        return Err(Fault::new(format!(49            "level {root} lies above the tree's depth {depth}."50        )));51    }52    let seat = index.unwrap_or(0) as usize;53    if seat >= tree.levels[root].len() {54        return Err(Fault::new(format!(55            "level {root} holds {} boxes, so there is no box {seat}.",56            tree.levels[root].len()57        )));58    }59    Ok((root, seat))60}6162fn spans(tree: &Shell, root: usize, seat: usize) -> Vec<(usize, usize)> {63    let mut out = vec![(0usize, 0usize); root + 1];64    out[root] = (seat, seat + 1);65    for level in (0..root).rev() {66        let (lo, hi) = out[level + 1];67        let row = &tree.levels[level];68        let held = |k: usize| row[k].parent >= lo && row[k].parent < hi;69        let start = (0..row.len()).find(|&k| held(k)).unwrap_or(row.len());70        let mut stop = start;71        while stop < row.len() && held(stop) {72            stop += 1;73        }74        out[level] = (start, stop);75    }76    out77}7879/// Reads the crossing shell of one radius as a rooted tree: its depth, its leaves and one row per level.80///81/// Every row carries the boxes the circle crosses at that level, the count `2 * floor(radius / number^level) + 1` the identity asks for, the live boxes whose path never takes a seat the design drops, and the mean number of children a box of the level holds. `exact` is true when every level meets its count and no box lost its parent, so one flag says whether the drawing and the mathematics agree.82#[wasm_bindgen]83pub fn shell_read(code: &str, number: usize, base: usize, radius: u32) -> Result<String, Fault> {84    let (tree, depth) = guarded(code, number, base, radius)?;85    let scale = |level: usize| (number as u64).pow(level as u32);86    let mut rows = Vec::with_capacity(depth + 1);87    let mut exact = tree.orphans == 0;88    for level in 0..=depth {89        let boxes = tree.levels[level].len() as u64;90        let want = 2 * (radius as u64 / scale(level)) + 1;91        exact = exact && boxes == want;92        let live = tree.levels[level].iter().filter(|cell| cell.live).count();93        let below = if level == 0 {94            0.095        } else {96            tree.levels[level - 1].len() as f64 / boxes as f6497        };98        rows.push(json!({99            "level": level,100            "boxes": boxes,101            "want": want,102            "live": live,103            "branch": below,104            "three": level > 0 && (radius as u64 / scale(level - 1)) % 3 == 1,105        }));106    }107    Ok(json!({108        "radius": radius,109        "number": number,110        "depth": depth,111        "side": scale(depth),112        "leaves": tree.levels[0].len(),113        "live": tree.levels[0].iter().filter(|cell| cell.live).count(),114        "orphans": tree.orphans,115        "exact": exact,116        "levels": rows,117    })118    .to_string())119}120121/// Lays the crossing tree out flat for drawing: the level, both coordinates, the parent's place in the level above and the live flag, five numbers a box, the crossed cells first and the root last.122///123/// `level` and `index` name the box the walk is rooted at, and without them the walk is the whole tree rooted at its own single top box. A box's children are contiguous among its level, so a subtree is one range a level and the parent's place is counted from the start of that range, which lets a page draw one branch of a wide tree with every count in it still exact.124///125/// A box that lost its parent carries the largest number the type holds in that place, which the shell identity forbids and `shell_read` counts.126#[wasm_bindgen]127pub fn shell_nodes(128    code: &str,129    number: usize,130    base: usize,131    radius: u32,132    level: Option<u32>,133    index: Option<u32>,134) -> Result<Vec<u32>, Fault> {135    let (tree, depth) = guarded(code, number, base, radius)?;136    let (root, seat) = seated(&tree, depth, level, index)?;137    let spans = spans(&tree, root, seat);138    let mut out = Vec::new();139    for level in 0..=root {140        let (lo, hi) = spans[level];141        for k in lo..hi {142            let cell = tree.levels[level][k];143            let parent = if level == root {144                usize::MAX145            } else {146                cell.parent.wrapping_sub(spans[level + 1].0)147            };148            out.push(level as u32);149            out.push(cell.x as u32);150            out.push(cell.y as u32);151            out.push(parent.min(u32::MAX as usize) as u32);152            out.push(u32::from(cell.live));153        }154    }155    Ok(out)156}157158/// Paints the design at the tree's own depth with the crossed cells lit, the pruned ones in their own ink and one level's boxes outlined.159///160/// The grid is the design at the least level that holds the circle, so one cell is one leaf of the tree and the circle's own corner sits at the bottom left: the design's other cells are the faint ground, a crossed cell the design keeps is gold, a crossed cell it drops is blue, and the boxes of level `at` are outlined under the crossed cells so the `2 * floor(radius / number^at) + 1` of them can be counted on the picture. The sheet is the same width at every depth, a whole number of pixels to the cell, so the outline stays a hairline however deep the tree runs.161///162/// `root` and `pick` name one box to ring in its own ink over everything else, so a page drawing one branch of the tree can show which box of the circle that branch is.163#[wasm_bindgen]164pub fn shell_pixels(165    code: &str,166    number: usize,167    base: usize,168    radius: u32,169    at: u32,170    root: Option<u32>,171    pick: Option<u32>,172) -> Result<Pixels, Fault> {173    let (tree, depth) = guarded(code, number, base, radius)?;174    if at as usize > depth {175        return Err(Fault::new(format!(176            "level {at} lies above the tree's depth {depth}."177        )));178    }179    let ringed = match (root, pick) {180        (Some(level), Some(index)) => Some(seated(&tree, depth, Some(level), Some(index))?),181        _ => None,182    };183    let grid = body(code, number, base, depth)?;184    let side = grid.shape[0];185    let scale = (SHEET / side).max(1);186    let wide = side * scale;187    let ink = theme();188    let mut colors = vec![rgba(ink.ground); wide * wide];189    let mut block = |x0: usize, y0: usize, x1: usize, y1: usize, color: [u8; 4]| {190        for row in (wide - x1.min(wide))..(wide - x0.min(wide)) {191            for column in y0.min(wide)..y1.min(wide) {192                colors[row * wide + column] = color;193            }194        }195    };196    for x in 0..side {197        for y in 0..side {198            if grid.at(x * side + y) != 0 {199                block(200                    x * scale,201                    y * scale,202                    (x + 1) * scale,203                    (y + 1) * scale,204                    rgba(ink.line),205                );206            }207        }208    }209    let step = number.pow(at) * scale;210    for cell in &tree.levels[at as usize] {211        let (x0, y0) = (cell.x as usize * step, cell.y as usize * step);212        let (x1, y1) = (x0 + step, y0 + step);213        let pink = rgba(ink.pink);214        block(x0, y0, x1, y0 + EDGE, pink);215        block(x0, y1 - EDGE, x1, y1, pink);216        block(x0, y0, x0 + EDGE, y1, pink);217        block(x1 - EDGE, y0, x1, y1, pink);218    }219    for cell in &tree.levels[0] {220        let (x0, y0) = (cell.x as usize * scale, cell.y as usize * scale);221        let color = rgba(if cell.live { ink.yellow } else { ink.blue });222        block(x0, y0, x0 + scale, y0 + scale, color);223    }224    if let Some((level, seat)) = ringed {225        let cell = tree.levels[level][seat];226        let span = number.pow(level as u32) * scale;227        let (x0, y0) = (cell.x as usize * span, cell.y as usize * span);228        let (x1, y1) = (x0 + span, y0 + span);229        let green = rgba(ink.green);230        block(x0, y0, x1, y0 + MARK, green);231        block(x0, y1.saturating_sub(MARK), x1, y1, green);232        block(x0, y0, x0 + MARK, y1, green);233        block(x1.saturating_sub(MARK), y0, x1, y1, green);234    }235    Ok(Pixels::of(wide, wide, colors))236}