three.rs

5.7 kB · rust · 184 lines

1use crate::{code_of, Fault};2use mrlycore::json;3use mrlymath::formulas;4use mrlymath::space::Pack;5use mrlymath::three::{self, Cell3d};6use wasm_bindgen::prelude::*;78fn cell(code: &str, number: usize, level: usize, base: usize) -> Result<Cell3d, Fault> {9    Ok(three::create(code_of(code)?, number, level, base)?)10}1112/// Packs the exposed faces of the cube the code names: two section lengths, then six floats per vertex, position and normal, in the unit box.13#[wasm_bindgen]14pub fn three_faces(15    code: &str,16    number: usize,17    level: usize,18    base: usize,19) -> Result<Vec<f32>, Fault> {20    let mut pack = Pack::new();21    for quad in three::quads(&cell(code, number, level, base)?) {22        pack.quad(quad.verts, quad.normal);23    }24    Ok(pack.buffer())25}2627/// Lists the filled sites of the cube the code names as x, y, z triples.28#[wasm_bindgen]29pub fn three_cells(30    code: &str,31    number: usize,32    level: usize,33    base: usize,34) -> Result<Vec<u32>, Fault> {35    let cell = cell(code, number, level, base)?;36    let grid = cell.types();37    let mut out = Vec::new();38    for (flat, &site) in grid.bytes().iter().enumerate() {39        if site != 0 {40            let (i, rest) = (41                flat / (grid.shape[1] * grid.shape[2]),42                flat % (grid.shape[1] * grid.shape[2]),43            );44            out.extend([45                i as u32,46                (rest / grid.shape[2]) as u32,47                (rest % grid.shape[2]) as u32,48            ]);49        }50    }51    Ok(out)52}5354/// Tallies the cube: fills, voids, surface, vertices, edges, faces and Euler number, as JSON.55#[wasm_bindgen]56pub fn three_census(code: &str, number: usize, level: usize, base: usize) -> Result<String, Fault> {57    let tally = three::census(&cell(code, number, level, base)?)?;58    Ok(json!({59        "fills": tally.fills,60        "voids": tally.voids,61        "surface": tally.surface.to_string(),62        "vertices": tally.vertices,63        "edges": tally.edges,64        "faces": tally.faces,65        "euler": tally.euler,66    })67    .to_string())68}6970/// Counts the exposed faces of the cube at the level by exact recurrence, without building it.71#[wasm_bindgen]72pub fn three_surface(code: &str, number: usize, level: u32, base: usize) -> Result<String, Fault> {73    Ok(formulas::surface(code_of(code)?, number, level, base)?.to_string())74}7576// DIAGONAL7778fn depth(level: usize) -> Result<(), Fault> {79    if !(1..=40).contains(&level) {80        return Err(Fault::new("level must be between 1 and 40."));81    }82    Ok(())83}8485/// Profiles the diagonal cut of the cube: the support, its central pair of heights, the count on every height inside it, the extremes and whether the cut is constant, as JSON.86#[wasm_bindgen]87pub fn diagonal_profile(88    code: &str,89    number: usize,90    level: usize,91    base: usize,92) -> Result<String, Fault> {93    depth(level)?;94    let counts = three::profile(code_of(code)?, number, level, base)?;95    let (low, high) = three::support(&counts)96        .ok_or_else(|| Fault::new(format!("code {code} fills no cell, so it has no cut.")))?;97    let span = &counts[low..=high];98    let live: Vec<u128> = span.iter().copied().filter(|&count| count > 0).collect();99    let least = *live.iter().min().unwrap();100    let most = *live.iter().max().unwrap();101    let mid = (low + high) / 2;102    Ok(json!({103        "side": number.pow(level as u32),104        "support": [low, high],105        "central": [mid, high.min(mid + 1)],106        "counts": span.iter().map(|count| count.to_string()).collect::<Vec<String>>(),107        "nonempty": live.len(),108        "heights": span.len(),109        "min": least.to_string(),110        "max": most.to_string(),111        "constant": live.len() == span.len() && least == most,112    })113    .to_string())114}115116/// Counts the filled cells on one diagonal plane of the cube, without building it.117#[wasm_bindgen]118pub fn diagonal_count(119    code: &str,120    number: usize,121    level: usize,122    base: usize,123    height: usize,124) -> Result<String, Fault> {125    depth(level)?;126    let counts = three::profile(code_of(code)?, number, level, base)?;127    Ok(counts.get(height).copied().unwrap_or(0).to_string())128}129130/// Spells the height's offset above the cut's support in binary, the digits that say which corners each scale may use.131#[wasm_bindgen]132pub fn diagonal_digits(133    code: &str,134    number: usize,135    level: usize,136    base: usize,137    height: usize,138) -> Result<String, Fault> {139    depth(level)?;140    let counts = three::profile(code_of(code)?, number, level, base)?;141    let (low, _) = three::support(&counts)142        .ok_or_else(|| Fault::new(format!("code {code} fills no cell, so it has no cut.")))?;143    Ok(format!("{:b}", height.saturating_sub(low)))144}145146/// Counts the filled cells on the named diagonal planes together, one per circle the drawing holds, as a decimal string.147#[wasm_bindgen]148pub fn diagonal_total(149    code: &str,150    number: usize,151    level: usize,152    base: usize,153    heights: Vec<u32>,154) -> Result<String, Fault> {155    depth(level)?;156    let counts = three::profile(code_of(code)?, number, level, base)?;157    let total: u128 = heights158        .iter()159        .map(|&height| counts.get(height as usize).copied().unwrap_or(0))160        .sum();161    Ok(total.to_string())162}163164/// Draws the named diagonal slices of the cube down the `(1,1,1)` axis, one circle per cell, as SVG.165#[wasm_bindgen]166pub fn diagonal_svg(167    code: &str,168    number: usize,169    level: usize,170    base: usize,171    heights: Vec<u32>,172    scale: usize,173) -> Result<String, Fault> {174    depth(level)?;175    let heights: Vec<usize> = heights.iter().map(|&height| height as usize).collect();176    Ok(three::diagonal_svg(177        code_of(code)?,178        number,179        level,180        base,181        &heights,182        scale,183    )?)184}