crop.rs

19.0 kB · rust · 578 lines

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