#![allow(clippy::too_many_arguments)] use crate::{code_of, theme, Fault, Grid}; use mrlycore::json; use mrlycore::tensor::Tensor; use mrlymath::bang::factory; use mrlymath::formulas; use mrlymath::shape::{self, Frac, Shape}; use mrlymath::space::Pack; use mrlymath::three::{self, Cell3d}; use wasm_bindgen::prelude::*; fn radius_of(rnum: u32, rden: u32) -> Result { if rden == 0 { return Err(Fault::new("the radius denominator must be at least 1.")); } Ok(Frac::new(rnum as i64, rden as i64)) } fn shape_of( name: &str, dimension: usize, rnum: u32, rden: u32, anti: bool, ) -> Result { let core = shape::named(name, dimension, radius_of(rnum, rden)?)?; Ok(if anti { Shape::Anti(Box::new(core)) } else { core }) } fn keep_of(policy: &str) -> Result { match policy { "inside" => Ok(false), "touching" => Ok(true), _ => Err(Fault::new(format!( "policy {policy:?} is not inside or touching." ))), } } fn design( code: &str, number: usize, dimension: usize, base: usize, level: usize, ) -> Result { Ok(factory::create( code_of(code)?, number, dimension, base, level, )?) } /// Lists the named shapes a crop can take in the dimension, as a JSON array. #[wasm_bindgen] pub fn crop_shapes(dimension: usize) -> String { json!(shape::shapes(dimension)).to_string() } /// Crops the flat design to the shape as a byte grid: inside and touching keep coarse cells, refined1 and refined2 rebuild the rim on a finer lattice. #[wasm_bindgen] pub fn crop_grid( code: &str, number: usize, level: usize, base: usize, shape: &str, rnum: u32, rden: u32, anti: bool, policy: &str, ) -> Result { let types = design(code, number, 2, base, level)?; let shape = shape_of(shape, 2, rnum, rden, anti)?; let kept = match policy { "refined1" => shape::refine(&types, &shape, number, 1, false)?, "refined2" => shape::refine(&types, &shape, number, 2, false)?, _ => shape::crop(&types, &shape, keep_of(policy)?), }; Ok(Grid { width: kept.shape[1] as u32, height: kept.shape[0] as u32, types: kept.bytes().to_vec(), }) } /// Lists the filled cells of the cube design kept by the shape as x, y, z triples. #[wasm_bindgen] pub fn crop_cells( code: &str, number: usize, level: usize, base: usize, shape: &str, rnum: u32, rden: u32, anti: bool, policy: &str, ) -> Result, Fault> { let kept = cropped_cube(code, number, level, base, shape, rnum, rden, anti, policy)?; let grid = kept.types(); let mut out = Vec::new(); for (flat, &site) in grid.bytes().iter().enumerate() { if site != 0 { let (i, rest) = ( flat / (grid.shape[1] * grid.shape[2]), flat % (grid.shape[1] * grid.shape[2]), ); out.extend([ i as u32, (rest / grid.shape[2]) as u32, (rest % grid.shape[2]) as u32, ]); } } Ok(out) } /// Packs the exposed faces of the cropped cube, capping the cut with correct normals: two section lengths, then six floats per vertex, position and normal. #[wasm_bindgen] pub fn crop_faces( code: &str, number: usize, level: usize, base: usize, shape: &str, rnum: u32, rden: u32, anti: bool, policy: &str, ) -> Result, Fault> { let kept = cropped_cube(code, number, level, base, shape, rnum, rden, anti, policy)?; let mut pack = Pack::new(); for quad in three::quads(&kept) { pack.quad(quad.verts, quad.normal); } Ok(pack.buffer()) } fn cropped_cube( code: &str, number: usize, level: usize, base: usize, shape: &str, rnum: u32, rden: u32, anti: bool, policy: &str, ) -> Result { let types = design(code, number, 3, base, level)?; let shape = shape_of(shape, 3, rnum, rden, anti)?; Ok(Cell3d::new(shape::crop(&types, &shape, keep_of(policy)?))) } /// Tallies the design against the shape: cells and fills per region, and the exposed measure before and after the touching crop, as JSON. #[wasm_bindgen] pub fn crop_census( code: &str, number: usize, level: usize, base: usize, dimension: usize, shape: &str, rnum: u32, rden: u32, anti: bool, ) -> Result { let types = design(code, number, dimension, base, level)?; let shape = shape_of(shape, dimension, rnum, rden, anti)?; let tally = shape::census(&shape, &types); let after = shape::crop(&types, &shape, true); Ok(json!({ "cells_out": tally.cells[0], "cells_cut": tally.cells[1], "cells_in": tally.cells[2], "filled_out": tally.filled[0], "filled_cut": tally.filled[1], "filled_in": tally.filled[2], "exposed_before": mrlynum::census::exposed(&types).to_string(), "exposed_after": mrlynum::census::exposed(&after).to_string(), }) .to_string()) } const SERIES_CELLS: usize = 1_000_000; fn series_guard(number: usize, dimension: usize, level: usize, steps: usize) -> Result<(), Fault> { let stop = || Fault::new(format!("the series would build over {SERIES_CELLS} cells.")); if !(1..=64).contains(&steps) { return Err(Fault::new("steps must be between 1 and 64.")); } let side = number.checked_pow(level as u32).ok_or_else(stop)?; let cells = side.checked_pow(dimension as u32).ok_or_else(stop)?; if cells > SERIES_CELLS { return Err(stop()); } Ok(()) } /// Sweeps the crop along one axis for charts: level walks the depth at the radius, radius walks the fraction 1 over steps to 1 at the level; each entry carries x, filled_in, filled_cut and exposed_after, as JSON. #[wasm_bindgen] pub fn crop_series( code: &str, number: usize, level: usize, base: usize, dimension: usize, shape: &str, rnum: u32, rden: u32, anti: bool, axis: &str, steps: usize, ) -> Result { let entry = |x: f64, types: &Tensor, s: &Shape| { let tally = shape::census(s, types); let after = shape::crop(types, s, true); json!({ "x": x, "filled_in": tally.filled[2], "filled_cut": tally.filled[1], "exposed_after": mrlynum::census::exposed(&after).to_string(), }) }; let mut rows = Vec::new(); match axis { "level" => { series_guard(number, dimension, steps, steps)?; let s = shape_of(shape, dimension, rnum, rden, anti)?; for depth in 0..=steps { let types = if depth == 0 { Tensor::full(vec![1; dimension], 1) } else { design(code, number, dimension, base, depth)? }; rows.push(entry(depth as f64, &types, &s)); } } "radius" => { series_guard(number, dimension, level, steps)?; let types = design(code, number, dimension, base, level)?; for num in 1..=steps { let s = shape_of(shape, dimension, num as u32, steps as u32, anti)?; rows.push(entry(num as f64 / steps as f64, &types, &s)); } } _ => return Err(Fault::new(format!("axis {axis:?} is not level or radius."))), } Ok(json!(rows).to_string()) } fn circle_table( code: &str, number: usize, level: usize, base: usize, dimension: usize, centre: &str, ) -> Result, Fault> { let stop = || Fault::new(format!("that count would build over {SERIES_CELLS} cells.")); let side = number.checked_pow(level as u32).ok_or_else(stop)?; if side.checked_pow(dimension as u32).ok_or_else(stop)? > SERIES_CELLS { return Err(stop()); } let (origin, r_max) = match centre { "corner" => (vec![0i64; dimension], side as u64 - 1), "centre" => (vec![side as i64; dimension], (side as u64 - 1) / 2), _ => { return Err(Fault::new(format!( "centre {centre:?} is not corner or centre." ))) } }; let types = design(code, number, dimension, base, level)?; Ok(shape::radial_census(&types, &origin, r_max)) } /// Counts the design's filled cells against every integer radius about the corner or the grid centre. /// /// The corner ball runs to the radius side minus one, the centre ball to half of that; the reply lays the seen, the inside and the cut prefix arrays end to end, each a third of the length and indexed by radius from zero. #[wasm_bindgen] pub fn crop_circle( code: &str, number: usize, level: usize, base: usize, dimension: usize, centre: &str, ) -> Result, Fault> { let table = circle_table(code, number, level, base, dimension, centre)?; let mut out = Vec::with_capacity(3 * table.len()); out.extend(table.iter().map(|row| row.seen as u32)); out.extend(table.iter().map(|row| row.inside as u32)); out.extend(table.iter().map(|row| row.cut as u32)); Ok(out) } /// Folds the radial count into one profile per scale, so two radius regimes lie on each other. /// /// Scale `k` is the window of radii from `number^k` to `number^(k + 1) - 1`, kept only when the whole window is counted; its profile reads the count over `t^d` at `samples` points of `t = number^(k + j / samples)` with the radius `floor(t)`, so every scale is read at the same offsets of `log_number r` and the profiles are comparable point by point. /// /// Every scale also folds the defect `N(number * r) - fill * N(r)` over `t^(d - 1)` into `drift`, read at the same offsets, and that ridge is null on the scales whose window runs past the counted radii. /// /// Each scale carries its mean level and its mean ridge, and each pair of scales the largest and the mean gap between their profiles, with that largest gap over the greater of the two levels and the same pair of numbers for the ridges. #[wasm_bindgen] pub fn crop_collapse( code: &str, number: usize, level: usize, base: usize, dimension: usize, centre: &str, samples: usize, ) -> Result { if !(2..=256).contains(&samples) { return Err(Fault::new("samples must be between 2 and 256.")); } if number < 2 { return Err(Fault::new("the side number must be at least 2 to fold.")); } let table = circle_table(code, number, level, base, dimension, centre)?; let seen: Vec = table.iter().map(|row| row.seen as f64).collect(); let top = seen.len() - 1; let mass = formulas::fill(code_of(code)?, number, dimension, 1, base)?; let d = formulas::dimension(code_of(code)?, number, dimension, base)?; let step = number as f64; let mut scales = Vec::new(); let (mut mains, mut levels) = (Vec::new(), Vec::new()); let (mut drifts, mut ridges) = (Vec::new(), Vec::new()); let mut start = 1usize; while start * number <= top + 1 { let stop = start * number - 1; let folded = number * stop <= top; let mut main = Vec::new(); let mut drift = Vec::new(); for j in 0..samples { let t = start as f64 * step.powf(j as f64 / samples as f64); let r = (t.floor() as usize).clamp(start, stop); main.push(seen[r] / t.powf(d)); if folded { let slip = seen[number * r] - mass as f64 * seen[r]; drift.push(slip.abs() / t.powf(d - 1.0)); } } let mean = main.iter().sum::() / samples as f64; let ridge = drift.iter().sum::() / samples as f64; scales.push(json!({ "start": start, "stop": stop, "level": mean, "ridge": if folded { json!(ridge) } else { json!(null) }, "main": main.clone(), "drift": drift.clone(), })); levels.push(mean); ridges.push(if folded { ridge } else { 0.0 }); mains.push(main); drifts.push(drift); start *= number; } let spread = |near: &[f64], far: &[f64]| { let (mut sup, mut total) = (0.0f64, 0.0f64); for (a, z) in near.iter().zip(far.iter()) { let gap = (a - z).abs(); sup = sup.max(gap); total += gap; } (sup, total / samples as f64) }; let mut pairs = Vec::new(); for low in 0..mains.len() { for high in low + 1..mains.len() { let (sup, mean) = spread(&mains[low], &mains[high]); let floor = levels[low].max(levels[high]); let ridged = !drifts[low].is_empty() && !drifts[high].is_empty(); let (rsup, _) = spread(&drifts[low], &drifts[high]); let bar = ridges[low].max(ridges[high]); pairs.push(json!({ "low": low, "high": high, "sup": sup, "mean": mean, "share": if floor > 0.0 { sup / floor } else { 0.0 }, "rsup": if ridged { json!(rsup) } else { json!(null) }, "rshare": if ridged && bar > 0.0 { json!(rsup / bar) } else { json!(null) }, })); } } Ok(json!({ "d": d, "mass": mass.to_string(), "top": top, "samples": samples, "scales": scales, "pairs": pairs, }) .to_string()) } fn outline(name: &str, r: Frac) -> Option> { let h = Frac::new(1, 2); let q = r * h; let m = Frac::whole(0) - r; let mq = Frac::whole(0) - q; let shifted = |points: Vec<[Frac; 2]>| points.iter().map(|[a, b]| [h + *a, h + *b]).collect(); match name { "box" => Some(shifted(vec![[m, m], [m, r], [r, r], [r, m]])), "diamond" => Some(shifted(vec![ [m, Frac::whole(0)], [Frac::whole(0), r], [r, Frac::whole(0)], [Frac::whole(0), m], ])), "triangle" => Some(shifted(vec![[m, Frac::whole(0)], [r, m], [r, r]])), "octagon" => Some(shifted(vec![ [m, mq], [m, q], [mq, r], [q, r], [r, q], [r, mq], [q, m], [mq, m], ])), _ => None, } } /// Draws the flat crop as SVG: the touched cells as rects, trimmed to the exact shape by a clip path, or by a mask when the crop is an anti-crop. #[wasm_bindgen] pub fn crop_svg( code: &str, number: usize, level: usize, base: usize, shape: &str, rnum: u32, rden: u32, anti: bool, scale: usize, ) -> Result { let types = design(code, number, 2, base, level)?; let cropper = shape_of(shape, 2, rnum, rden, anti)?; let kept = shape::crop(&types, &cropper, true); let r = radius_of(rnum, rden)?; let side = kept.shape[0]; let span = side * scale; let px = |f: Frac| f.num as f64 * span as f64 / f.den as f64; let element = match outline(shape, r) { Some(points) => { let listed: Vec = points .iter() .map(|[a0, a1]| format!("{},{}", px(*a1), px(*a0))) .collect(); format!(" { let centre = px(Frac::new(1, 2)); format!("" )]; if anti { out.push(format!( "{element} fill=\"black\"/>" )); out.push("".to_string()); } else { out.push(format!("{element}/>")); out.push("".to_string()); } let ground = theme().ground.to_hex(); for a0 in 0..side { for a1 in 0..side { if kept.get(&[a0, a1]) == 0 { continue; } let (x, y) = (a1 * scale, a0 * scale); out.push(format!( "" )); } } out.push("".to_string()); out.push("".to_string()); Ok(out.join("\n")) } fn holds(shape: &Shape, side: usize, index: &[usize]) -> bool { match shape { Shape::Ball { center, radius } => { if radius.num < 0 { return false; } let l = center.iter().fold(radius.den, |acc, c| { acc / mrlynum::classics::gcd(acc as u128, c.den as u128) as i64 * c.den }); let scale = 2 * side as i128 * l as i128; let rr = radius.num as i128 * (scale / radius.den as i128); let mut gap: i128 = 0; for (axis, c) in center.iter().enumerate() { let p = (2 * index[axis] as i128 + 1) * l as i128; let cc = c.num as i128 * (scale / c.den as i128); let d = p - cc; gap += d * d; } gap <= rr * rr } Shape::Polytope { walls } => walls.iter().all(|wall| { let form: i128 = wall .normal .iter() .enumerate() .map(|(axis, &n)| n as i128 * (2 * index[axis] as i128 + 1)) .sum(); form * wall.offset.den as i128 <= wall.offset.num as i128 * 2 * side as i128 }), Shape::Anti(inner) => !holds(inner, side, index), } } /// Masks a float field for display: a copy with NaN wherever the cell centre falls outside the kept region. #[wasm_bindgen] pub fn field_crop( data: &[f32], size: usize, dimension: usize, shape: &str, rnum: u32, rden: u32, anti: bool, ) -> Result, Fault> { if !(2..=3).contains(&dimension) { return Err(Fault::new("the dimension must be 2 or 3.")); } let want = size.checked_pow(dimension as u32).ok_or_else(|| { Fault::new("the field must hold size to the dimension samples with size at least 1.") })?; if size == 0 || data.len() != want { return Err(Fault::new( "the field must hold size to the dimension samples with size at least 1.", )); } let cropper = shape_of(shape, dimension, rnum, rden, anti)?; let mut index = vec![0usize; dimension]; Ok(data .iter() .enumerate() .map(|(flat, &value)| { let mut rem = flat; for axis in (0..dimension).rev() { index[axis] = rem % size; rem /= size; } if holds(&cropper, size, &index) { value } else { f32::NAN } }) .collect()) }