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}