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}