shell.rs

9.1 kB · rust · 229 lines

1use crate::{code_of, rgba, theme, Fault, Pixels};2use mrlycore::json;3use mrlycore::tensor::Tensor;4use mrlymath::bang::factory;5use mrlymath::shape::{crossing_tree, Shell};6use wasm_bindgen::prelude::*;78const RADIUS_CAP: u32 = 242;9const SHEET: usize = 486;10const EDGE: usize = 2;11const MARK: usize = 5;1213fn guarded(code: &str, number: usize, base: usize, radius: u32) -> Result<(Shell, usize), Fault> {14    if number < 2 {15        return Err(Fault::new("the side number must be at least 2."));16    }17    if !(1..=RADIUS_CAP).contains(&radius) {18        return Err(Fault::new(format!(19            "the radius must be between 1 and {RADIUS_CAP} cells."20        )));21    }22    let tile = factory::create(code_of(code)?, number, 2, base, 1)?;23    let keep: Vec<bool> = tile.bytes().iter().map(|&b| b != 0).collect();24    let tree = crossing_tree(radius as u64, number as u64, &keep);25    let depth = tree.levels.len() - 1;26    Ok((tree, depth))27}2829fn body(code: &str, number: usize, base: usize, depth: usize) -> Result<Tensor, Fault> {30    Ok(factory::create(code_of(code)?, number, 2, base, depth)?)31}3233fn seated(34    tree: &Shell,35    depth: usize,36    level: Option<u32>,37    index: Option<u32>,38) -> Result<(usize, usize), Fault> {39    let root = level.map_or(depth, |j| j as usize);40    if root > depth {41        return Err(Fault::new(format!(42            "level {root} lies above the tree's depth {depth}."43        )));44    }45    let seat = index.unwrap_or(0) as usize;46    if seat >= tree.levels[root].len() {47        return Err(Fault::new(format!(48            "level {root} holds {} boxes, so there is no box {seat}.",49            tree.levels[root].len()50        )));51    }52    Ok((root, seat))53}5455fn spans(tree: &Shell, root: usize, seat: usize) -> Vec<(usize, usize)> {56    let mut out = vec![(0usize, 0usize); root + 1];57    out[root] = (seat, seat + 1);58    for level in (0..root).rev() {59        let (lo, hi) = out[level + 1];60        let row = &tree.levels[level];61        let held = |k: usize| row[k].parent >= lo && row[k].parent < hi;62        let start = (0..row.len()).find(|&k| held(k)).unwrap_or(row.len());63        let mut stop = start;64        while stop < row.len() && held(stop) {65            stop += 1;66        }67        out[level] = (start, stop);68    }69    out70}7172/// Reads the crossing shell of one radius as a rooted tree: its depth, its leaves and one row per level.73///74/// 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.75#[wasm_bindgen]76pub fn shell_read(code: &str, number: usize, base: usize, radius: u32) -> Result<String, Fault> {77    let (tree, depth) = guarded(code, number, base, radius)?;78    let scale = |level: usize| (number as u64).pow(level as u32);79    let mut rows = Vec::with_capacity(depth + 1);80    let mut exact = tree.orphans == 0;81    for level in 0..=depth {82        let boxes = tree.levels[level].len() as u64;83        let want = 2 * (radius as u64 / scale(level)) + 1;84        exact = exact && boxes == want;85        let live = tree.levels[level].iter().filter(|cell| cell.live).count();86        let below = if level == 0 {87            0.088        } else {89            tree.levels[level - 1].len() as f64 / boxes as f6490        };91        rows.push(json!({92            "level": level,93            "boxes": boxes,94            "want": want,95            "live": live,96            "branch": below,97            "three": level > 0 && (radius as u64 / scale(level - 1)) % 3 == 1,98        }));99    }100    Ok(json!({101        "radius": radius,102        "number": number,103        "depth": depth,104        "side": scale(depth),105        "leaves": tree.levels[0].len(),106        "live": tree.levels[0].iter().filter(|cell| cell.live).count(),107        "orphans": tree.orphans,108        "exact": exact,109        "levels": rows,110    })111    .to_string())112}113114/// 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.115///116/// `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.117///118/// 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.119#[wasm_bindgen]120pub fn shell_nodes(121    code: &str,122    number: usize,123    base: usize,124    radius: u32,125    level: Option<u32>,126    index: Option<u32>,127) -> Result<Vec<u32>, Fault> {128    let (tree, depth) = guarded(code, number, base, radius)?;129    let (root, seat) = seated(&tree, depth, level, index)?;130    let spans = spans(&tree, root, seat);131    let mut out = Vec::new();132    for level in 0..=root {133        let (lo, hi) = spans[level];134        for k in lo..hi {135            let cell = tree.levels[level][k];136            let parent = if level == root {137                usize::MAX138            } else {139                cell.parent.wrapping_sub(spans[level + 1].0)140            };141            out.push(level as u32);142            out.push(cell.x as u32);143            out.push(cell.y as u32);144            out.push(parent.min(u32::MAX as usize) as u32);145            out.push(u32::from(cell.live));146        }147    }148    Ok(out)149}150151/// 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.152///153/// 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.154///155/// `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.156#[wasm_bindgen]157pub fn shell_pixels(158    code: &str,159    number: usize,160    base: usize,161    radius: u32,162    at: u32,163    root: Option<u32>,164    pick: Option<u32>,165) -> Result<Pixels, Fault> {166    let (tree, depth) = guarded(code, number, base, radius)?;167    if at as usize > depth {168        return Err(Fault::new(format!(169            "level {at} lies above the tree's depth {depth}."170        )));171    }172    let ringed = match (root, pick) {173        (Some(level), Some(index)) => Some(seated(&tree, depth, Some(level), Some(index))?),174        _ => None,175    };176    let grid = body(code, number, base, depth)?;177    let side = grid.shape[0];178    let scale = (SHEET / side).max(1);179    let wide = side * scale;180    let ink = theme();181    let mut colors = vec![rgba(ink.ground); wide * wide];182    let mut block = |x0: usize, y0: usize, x1: usize, y1: usize, color: [u8; 4]| {183        for row in (wide - x1.min(wide))..(wide - x0.min(wide)) {184            for column in y0.min(wide)..y1.min(wide) {185                colors[row * wide + column] = color;186            }187        }188    };189    for x in 0..side {190        for y in 0..side {191            if grid.bytes()[x * side + y] != 0 {192                block(193                    x * scale,194                    y * scale,195                    (x + 1) * scale,196                    (y + 1) * scale,197                    rgba(ink.line),198                );199            }200        }201    }202    let step = number.pow(at) * scale;203    for cell in &tree.levels[at as usize] {204        let (x0, y0) = (cell.x as usize * step, cell.y as usize * step);205        let (x1, y1) = (x0 + step, y0 + step);206        let pink = rgba(ink.pink);207        block(x0, y0, x1, y0 + EDGE, pink);208        block(x0, y1 - EDGE, x1, y1, pink);209        block(x0, y0, x0 + EDGE, y1, pink);210        block(x1 - EDGE, y0, x1, y1, pink);211    }212    for cell in &tree.levels[0] {213        let (x0, y0) = (cell.x as usize * scale, cell.y as usize * scale);214        let color = rgba(if cell.live { ink.yellow } else { ink.blue });215        block(x0, y0, x0 + scale, y0 + scale, color);216    }217    if let Some((level, seat)) = ringed {218        let cell = tree.levels[level][seat];219        let span = number.pow(level as u32) * scale;220        let (x0, y0) = (cell.x as usize * span, cell.y as usize * span);221        let (x1, y1) = (x0 + span, y0 + span);222        let green = rgba(ink.green);223        block(x0, y0, x1, y0 + MARK, green);224        block(x0, y1.saturating_sub(MARK), x1, y1, green);225        block(x0, y0, x0 + MARK, y1, green);226        block(x1.saturating_sub(MARK), y0, x1, y1, green);227    }228    Ok(Pixels::of(wide, wide, colors))229}