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}