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}