crop.rs

18.8 kB · rust · 572 lines

1#![allow(clippy::too_many_arguments)]23use crate::{code_of, theme, Fault, Grid};4use mrlycore::json;5use mrlycore::tensor::Tensor;6use mrlymath::bang::factory;7use mrlymath::formulas;8use mrlymath::shape::{self, Frac, Shape};9use mrlymath::space::Pack;10use mrlymath::three::{self, Cell3d};11use wasm_bindgen::prelude::*;1213fn radius_of(rnum: u32, rden: u32) -> Result<Frac, Fault> {14    if rden == 0 {15        return Err(Fault::new("the radius denominator must be at least 1."));16    }17    Ok(Frac::new(rnum as i64, rden as i64))18}1920fn shape_of(21    name: &str,22    dimension: usize,23    rnum: u32,24    rden: u32,25    anti: bool,26) -> Result<Shape, Fault> {27    let core = shape::named(name, dimension, radius_of(rnum, rden)?)?;28    Ok(if anti {29        Shape::Anti(Box::new(core))30    } else {31        core32    })33}3435fn keep_of(policy: &str) -> Result<bool, Fault> {36    match policy {37        "inside" => Ok(false),38        "touching" => Ok(true),39        _ => Err(Fault::new(format!(40            "policy {policy:?} is not inside or touching."41        ))),42    }43}4445fn design(46    code: &str,47    number: usize,48    dimension: usize,49    base: usize,50    level: usize,51) -> Result<Tensor, Fault> {52    Ok(factory::create(53        code_of(code)?,54        number,55        dimension,56        base,57        level,58    )?)59}6061/// Lists the named shapes a crop can take in the dimension, as a JSON array.62#[wasm_bindgen]63pub fn crop_shapes(dimension: usize) -> String {64    json!(shape::shapes(dimension)).to_string()65}6667/// 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.68#[wasm_bindgen]69pub fn crop_grid(70    code: &str,71    number: usize,72    level: usize,73    base: usize,74    shape: &str,75    rnum: u32,76    rden: u32,77    anti: bool,78    policy: &str,79) -> Result<Grid, Fault> {80    let types = design(code, number, 2, base, level)?;81    let shape = shape_of(shape, 2, rnum, rden, anti)?;82    let kept = match policy {83        "refined1" => shape::refine(&types, &shape, number, 1, false)?,84        "refined2" => shape::refine(&types, &shape, number, 2, false)?,85        _ => shape::crop(&types, &shape, keep_of(policy)?),86    };87    Ok(Grid {88        width: kept.shape[1] as u32,89        height: kept.shape[0] as u32,90        types: kept.bytes().to_vec(),91    })92}9394/// Lists the filled cells of the cube design kept by the shape as x, y, z triples.95#[wasm_bindgen]96pub fn crop_cells(97    code: &str,98    number: usize,99    level: usize,100    base: usize,101    shape: &str,102    rnum: u32,103    rden: u32,104    anti: bool,105    policy: &str,106) -> Result<Vec<u32>, Fault> {107    let kept = cropped_cube(code, number, level, base, shape, rnum, rden, anti, policy)?;108    let grid = kept.types();109    let mut out = Vec::new();110    for (flat, &site) in grid.bytes().iter().enumerate() {111        if site != 0 {112            let (i, rest) = (113                flat / (grid.shape[1] * grid.shape[2]),114                flat % (grid.shape[1] * grid.shape[2]),115            );116            out.extend([117                i as u32,118                (rest / grid.shape[2]) as u32,119                (rest % grid.shape[2]) as u32,120            ]);121        }122    }123    Ok(out)124}125126/// 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.127#[wasm_bindgen]128pub fn crop_faces(129    code: &str,130    number: usize,131    level: usize,132    base: usize,133    shape: &str,134    rnum: u32,135    rden: u32,136    anti: bool,137    policy: &str,138) -> Result<Vec<f32>, Fault> {139    let kept = cropped_cube(code, number, level, base, shape, rnum, rden, anti, policy)?;140    let mut pack = Pack::new();141    for quad in three::quads(&kept) {142        pack.quad(quad.verts, quad.normal);143    }144    Ok(pack.buffer())145}146147fn cropped_cube(148    code: &str,149    number: usize,150    level: usize,151    base: usize,152    shape: &str,153    rnum: u32,154    rden: u32,155    anti: bool,156    policy: &str,157) -> Result<Cell3d, Fault> {158    let types = design(code, number, 3, base, level)?;159    let shape = shape_of(shape, 3, rnum, rden, anti)?;160    Ok(Cell3d::new(shape::crop(&types, &shape, keep_of(policy)?)))161}162163/// Tallies the design against the shape: cells and fills per region, and the exposed measure before and after the touching crop, as JSON.164#[wasm_bindgen]165pub fn crop_census(166    code: &str,167    number: usize,168    level: usize,169    base: usize,170    dimension: usize,171    shape: &str,172    rnum: u32,173    rden: u32,174    anti: bool,175) -> Result<String, Fault> {176    let types = design(code, number, dimension, base, level)?;177    let shape = shape_of(shape, dimension, rnum, rden, anti)?;178    let tally = shape::census(&shape, &types);179    let after = shape::crop(&types, &shape, true);180    Ok(json!({181        "cells_out": tally.cells[0],182        "cells_cut": tally.cells[1],183        "cells_in": tally.cells[2],184        "filled_out": tally.filled[0],185        "filled_cut": tally.filled[1],186        "filled_in": tally.filled[2],187        "exposed_before": mrlynum::census::exposed(&types).to_string(),188        "exposed_after": mrlynum::census::exposed(&after).to_string(),189    })190    .to_string())191}192193const SERIES_CELLS: usize = 1_000_000;194195fn series_guard(number: usize, dimension: usize, level: usize, steps: usize) -> Result<(), Fault> {196    let stop = || Fault::new(format!("the series would build over {SERIES_CELLS} cells."));197    if !(1..=64).contains(&steps) {198        return Err(Fault::new("steps must be between 1 and 64."));199    }200    let side = number.checked_pow(level as u32).ok_or_else(stop)?;201    let cells = side.checked_pow(dimension as u32).ok_or_else(stop)?;202    if cells > SERIES_CELLS {203        return Err(stop());204    }205    Ok(())206}207208/// 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.209#[wasm_bindgen]210pub fn crop_series(211    code: &str,212    number: usize,213    level: usize,214    base: usize,215    dimension: usize,216    shape: &str,217    rnum: u32,218    rden: u32,219    anti: bool,220    axis: &str,221    steps: usize,222) -> Result<String, Fault> {223    let entry = |x: f64, types: &Tensor, s: &Shape| {224        let tally = shape::census(s, types);225        let after = shape::crop(types, s, true);226        json!({227            "x": x,228            "filled_in": tally.filled[2],229            "filled_cut": tally.filled[1],230            "exposed_after": mrlynum::census::exposed(&after).to_string(),231        })232    };233    let mut rows = Vec::new();234    match axis {235        "level" => {236            series_guard(number, dimension, steps, steps)?;237            let s = shape_of(shape, dimension, rnum, rden, anti)?;238            for depth in 0..=steps {239                let types = if depth == 0 {240                    Tensor::full(vec![1; dimension], 1)241                } else {242                    design(code, number, dimension, base, depth)?243                };244                rows.push(entry(depth as f64, &types, &s));245            }246        }247        "radius" => {248            series_guard(number, dimension, level, steps)?;249            let types = design(code, number, dimension, base, level)?;250            for num in 1..=steps {251                let s = shape_of(shape, dimension, num as u32, steps as u32, anti)?;252                rows.push(entry(num as f64 / steps as f64, &types, &s));253            }254        }255        _ => return Err(Fault::new(format!("axis {axis:?} is not level or radius."))),256    }257    Ok(json!(rows).to_string())258}259260fn circle_table(261    code: &str,262    number: usize,263    level: usize,264    base: usize,265    dimension: usize,266    centre: &str,267) -> Result<Vec<shape::RadialCounts>, Fault> {268    let stop = || Fault::new(format!("that count would build over {SERIES_CELLS} cells."));269    let side = number.checked_pow(level as u32).ok_or_else(stop)?;270    if side.checked_pow(dimension as u32).ok_or_else(stop)? > SERIES_CELLS {271        return Err(stop());272    }273    let (origin, r_max) = match centre {274        "corner" => (vec![0i64; dimension], side as u64 - 1),275        "centre" => (vec![side as i64; dimension], (side as u64 - 1) / 2),276        _ => {277            return Err(Fault::new(format!(278                "centre {centre:?} is not corner or centre."279            )))280        }281    };282    let types = design(code, number, dimension, base, level)?;283    Ok(shape::radial_census(&types, &origin, r_max))284}285286/// Counts the design's filled cells against every integer radius about the corner or the grid centre.287///288/// 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.289#[wasm_bindgen]290pub fn crop_circle(291    code: &str,292    number: usize,293    level: usize,294    base: usize,295    dimension: usize,296    centre: &str,297) -> Result<Vec<u32>, Fault> {298    let table = circle_table(code, number, level, base, dimension, centre)?;299    let mut out = Vec::with_capacity(3 * table.len());300    out.extend(table.iter().map(|row| row.seen as u32));301    out.extend(table.iter().map(|row| row.inside as u32));302    out.extend(table.iter().map(|row| row.cut as u32));303    Ok(out)304}305306/// Folds the radial count into one profile per scale, so two radius regimes lie on each other.307///308/// 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.309///310/// 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.311///312/// 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.313#[wasm_bindgen]314pub fn crop_collapse(315    code: &str,316    number: usize,317    level: usize,318    base: usize,319    dimension: usize,320    centre: &str,321    samples: usize,322) -> Result<String, Fault> {323    if !(2..=256).contains(&samples) {324        return Err(Fault::new("samples must be between 2 and 256."));325    }326    if number < 2 {327        return Err(Fault::new("the side number must be at least 2 to fold."));328    }329    let table = circle_table(code, number, level, base, dimension, centre)?;330    let seen: Vec<f64> = table.iter().map(|row| row.seen as f64).collect();331    let top = seen.len() - 1;332    let mass = formulas::fill(code_of(code)?, number, dimension, 1, base)?;333    let d = formulas::dimension(code_of(code)?, number, dimension, base)?;334    let step = number as f64;335    let mut scales = Vec::new();336    let (mut mains, mut levels) = (Vec::new(), Vec::new());337    let (mut drifts, mut ridges) = (Vec::new(), Vec::new());338    let mut start = 1usize;339    while start * number <= top + 1 {340        let stop = start * number - 1;341        let folded = number * stop <= top;342        let mut main = Vec::new();343        let mut drift = Vec::new();344        for j in 0..samples {345            let t = start as f64 * step.powf(j as f64 / samples as f64);346            let r = (t.floor() as usize).clamp(start, stop);347            main.push(seen[r] / t.powf(d));348            if folded {349                let slip = seen[number * r] - mass as f64 * seen[r];350                drift.push(slip.abs() / t.powf(d - 1.0));351            }352        }353        let mean = main.iter().sum::<f64>() / samples as f64;354        let ridge = drift.iter().sum::<f64>() / samples as f64;355        scales.push(json!({356            "start": start,357            "stop": stop,358            "level": mean,359            "ridge": if folded { json!(ridge) } else { json!(null) },360            "main": main.clone(),361            "drift": drift.clone(),362        }));363        levels.push(mean);364        ridges.push(if folded { ridge } else { 0.0 });365        mains.push(main);366        drifts.push(drift);367        start *= number;368    }369    let spread = |near: &[f64], far: &[f64]| {370        let (mut sup, mut total) = (0.0f64, 0.0f64);371        for (a, z) in near.iter().zip(far.iter()) {372            let gap = (a - z).abs();373            sup = sup.max(gap);374            total += gap;375        }376        (sup, total / samples as f64)377    };378    let mut pairs = Vec::new();379    for low in 0..mains.len() {380        for high in low + 1..mains.len() {381            let (sup, mean) = spread(&mains[low], &mains[high]);382            let floor = levels[low].max(levels[high]);383            let ridged = !drifts[low].is_empty() && !drifts[high].is_empty();384            let (rsup, _) = spread(&drifts[low], &drifts[high]);385            let bar = ridges[low].max(ridges[high]);386            pairs.push(json!({387                "low": low,388                "high": high,389                "sup": sup,390                "mean": mean,391                "share": if floor > 0.0 { sup / floor } else { 0.0 },392                "rsup": if ridged { json!(rsup) } else { json!(null) },393                "rshare": if ridged && bar > 0.0 { json!(rsup / bar) } else { json!(null) },394            }));395        }396    }397    Ok(json!({398        "d": d,399        "mass": mass.to_string(),400        "top": top,401        "samples": samples,402        "scales": scales,403        "pairs": pairs,404    })405    .to_string())406}407408fn outline(name: &str, r: Frac) -> Option<Vec<[Frac; 2]>> {409    let h = Frac::new(1, 2);410    let q = r * h;411    let m = Frac::whole(0) - r;412    let mq = Frac::whole(0) - q;413    let shifted = |points: Vec<[Frac; 2]>| points.iter().map(|[a, b]| [h + *a, h + *b]).collect();414    match name {415        "box" => Some(shifted(vec![[m, m], [m, r], [r, r], [r, m]])),416        "diamond" => Some(shifted(vec![417            [m, Frac::whole(0)],418            [Frac::whole(0), r],419            [r, Frac::whole(0)],420            [Frac::whole(0), m],421        ])),422        "triangle" => Some(shifted(vec![[m, Frac::whole(0)], [r, m], [r, r]])),423        "octagon" => Some(shifted(vec![424            [m, mq],425            [m, q],426            [mq, r],427            [q, r],428            [r, q],429            [r, mq],430            [q, m],431            [mq, m],432        ])),433        _ => None,434    }435}436437/// 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.438#[wasm_bindgen]439pub fn crop_svg(440    code: &str,441    number: usize,442    level: usize,443    base: usize,444    shape: &str,445    rnum: u32,446    rden: u32,447    anti: bool,448    scale: usize,449) -> Result<String, Fault> {450    let types = design(code, number, 2, base, level)?;451    let cropper = shape_of(shape, 2, rnum, rden, anti)?;452    let kept = shape::crop(&types, &cropper, true);453    let r = radius_of(rnum, rden)?;454    let side = kept.shape[0];455    let span = side * scale;456    let px = |f: Frac| f.num as f64 * span as f64 / f.den as f64;457    let element = match outline(shape, r) {458        Some(points) => {459            let listed: Vec<String> = points460                .iter()461                .map(|[a0, a1]| format!("{},{}", px(*a1), px(*a0)))462                .collect();463            format!("<polygon points=\"{}\"", listed.join(" "))464        }465        None => {466            let centre = px(Frac::new(1, 2));467            format!("<circle cx=\"{centre}\" cy=\"{centre}\" r=\"{}\"", px(r))468        }469    };470    let mut out = vec![format!(471        "<svg width=\"{span}\" height=\"{span}\" xmlns=\"http://www.w3.org/2000/svg\">"472    )];473    if anti {474        out.push(format!(475            "<mask id=\"crop\"><rect width=\"{span}\" height=\"{span}\" fill=\"white\"/>{element} fill=\"black\"/></mask>"476        ));477        out.push("<g mask=\"url(#crop)\">".to_string());478    } else {479        out.push(format!("<clipPath id=\"crop\">{element}/></clipPath>"));480        out.push("<g clip-path=\"url(#crop)\">".to_string());481    }482    let ground = theme().ground.to_hex();483    for a0 in 0..side {484        for a1 in 0..side {485            if kept.get(&[a0, a1]) == 0 {486                continue;487            }488            let (x, y) = (a1 * scale, a0 * scale);489            out.push(format!(490                "<rect x=\"{x}\" y=\"{y}\" width=\"{scale}\" height=\"{scale}\" fill=\"{ground}\"/>"491            ));492        }493    }494    out.push("</g>".to_string());495    out.push("</svg>".to_string());496    Ok(out.join("\n"))497}498499fn holds(shape: &Shape, side: usize, index: &[usize]) -> bool {500    match shape {501        Shape::Ball { center, radius } => {502            if radius.num < 0 {503                return false;504            }505            let l = center.iter().fold(radius.den, |acc, c| {506                acc / mrlynum::classics::gcd(acc as u128, c.den as u128) as i64 * c.den507            });508            let scale = 2 * side as i128 * l as i128;509            let rr = radius.num as i128 * (scale / radius.den as i128);510            let mut gap: i128 = 0;511            for (axis, c) in center.iter().enumerate() {512                let p = (2 * index[axis] as i128 + 1) * l as i128;513                let cc = c.num as i128 * (scale / c.den as i128);514                let d = p - cc;515                gap += d * d;516            }517            gap <= rr * rr518        }519        Shape::Polytope { walls } => walls.iter().all(|wall| {520            let form: i128 = wall521                .normal522                .iter()523                .enumerate()524                .map(|(axis, &n)| n as i128 * (2 * index[axis] as i128 + 1))525                .sum();526            form * wall.offset.den as i128 <= wall.offset.num as i128 * 2 * side as i128527        }),528        Shape::Anti(inner) => !holds(inner, side, index),529    }530}531532/// Masks a float field for display: a copy with NaN wherever the cell centre falls outside the kept region.533#[wasm_bindgen]534pub fn field_crop(535    data: &[f32],536    size: usize,537    dimension: usize,538    shape: &str,539    rnum: u32,540    rden: u32,541    anti: bool,542) -> Result<Vec<f32>, Fault> {543    if !(2..=3).contains(&dimension) {544        return Err(Fault::new("the dimension must be 2 or 3."));545    }546    let want = size.checked_pow(dimension as u32).ok_or_else(|| {547        Fault::new("the field must hold size to the dimension samples with size at least 1.")548    })?;549    if size == 0 || data.len() != want {550        return Err(Fault::new(551            "the field must hold size to the dimension samples with size at least 1.",552        ));553    }554    let cropper = shape_of(shape, dimension, rnum, rden, anti)?;555    let mut index = vec![0usize; dimension];556    Ok(data557        .iter()558        .enumerate()559        .map(|(flat, &value)| {560            let mut rem = flat;561            for axis in (0..dimension).rev() {562                index[axis] = rem % size;563                rem /= size;564            }565            if holds(&cropper, size, &index) {566                value567            } else {568                f32::NAN569            }570        })571        .collect())572}