graph.rs
8.6 kB · rust · 285 lines
1use crate::{code_of, Fault};2use mrlycore::json;3use mrlymath::formulas::{self, six as hexagon};4use mrlymath::{six, three, two};5use mrlynum::graph::{self, census, roles, Layout as Relax, Network, Role};6use wasm_bindgen::prelude::*;78const LIMIT: u128 = 20000;9const DEEPEST: u32 = 40;10const ROOT3: f64 = 1.732_050_807_568_877_2;1112fn side(number: usize, level: usize) -> Result<usize, Fault> {13 number14 .checked_pow(level as u32)15 .ok_or_else(|| Fault::new("that level is deeper than a side counts."))16}1718fn dim_of(space: &str) -> Result<usize, Fault> {19 match space {20 "flat" | "hex" => Ok(2),21 "cube" => Ok(3),22 _ => Err(Fault::new(format!(23 "space {space:?} is none of \"flat\", \"cube\" and \"hex\"."24 ))),25 }26}2728fn kind_of(space: &str, kind: &str) -> Result<(), Fault> {29 let kinds: &[&str] = if space == "hex" {30 &["core", "dual", "edge"]31 } else {32 &["core", "edge", "tunnel"]33 };34 if kinds.contains(&kind) {35 return Ok(());36 }37 Err(Fault::new(format!(38 "graph {kind:?} is none of {} in the {space} space.",39 kinds.join(", ")40 )))41}4243fn bound(44 space: &str,45 code: &str,46 number: usize,47 level: u32,48 base: usize,49 kind: &str,50) -> Result<u128, Fault> {51 let dim = dim_of(space)?;52 kind_of(space, kind)?;53 let code = code_of(code)?;54 if space == "hex" {55 let side = side(number, level as usize)?;56 return Ok(match kind {57 "edge" => hexagon::solid_slice_vertices(side)?,58 _ => hexagon::grid_triangles(number, level),59 });60 }61 Ok(match kind {62 "core" => formulas::fill(code, number, dim, level, base)?,63 "tunnel" => formulas::void(code, number, dim, level, base)?,64 _ => formulas::fill(code, number, dim, level, base)? << dim,65 })66}6768/// Bounds the node count of the design's graph in closed form, before any build: the fill for the core, the void for the tunnels, `2^dim` fills for the edges, and the hexagon's triangles or corners for a slice, as a decimal string.69#[wasm_bindgen]70pub fn graph_size(71 space: &str,72 code: &str,73 number: usize,74 level: usize,75 base: usize,76 kind: &str,77) -> Result<String, Fault> {78 Ok(bound(space, code, number, level as u32, base, kind)?.to_string())79}8081/// Returns the largest level, at least one, whose graph bound stays within the budget, so a slider stops before a build stalls.82#[wasm_bindgen]83pub fn graph_cap(84 space: &str,85 code: &str,86 number: usize,87 base: usize,88 kind: &str,89 budget: usize,90) -> Result<usize, Fault> {91 bound(space, code, number, 1, base, kind)?;92 let fits = |level: u32| {93 bound(space, code, number, level, base, kind).is_ok_and(|count| count <= budget as u128)94 };95 let mut level = 1;96 while level < DEEPEST && fits(level + 1) {97 level += 1;98 }99 Ok(level as usize)100}101102fn network(103 space: &str,104 code: &str,105 number: usize,106 level: usize,107 base: usize,108 kind: &str,109) -> Result<(Network, Option<i64>), Fault> {110 let nodes = bound(space, code, number, level as u32, base, kind)?;111 if nodes > LIMIT {112 return Err(Fault::new(format!(113 "up to {nodes} nodes is past the {LIMIT} this page walks; lower the level."114 )));115 }116 let code = code_of(code)?;117 match space {118 "flat" => {119 let cell = two::create(code, number, level, 0, base)?;120 let net = match kind {121 "core" => two::graph::core_graph(&cell)?,122 "edge" => two::graph::edge_graph(&cell)?,123 _ => two::graph::tunnel_graph(&cell)?,124 };125 Ok((net, Some(two::census(&cell)?.euler)))126 }127 "cube" => {128 let cell = three::create(code, number, level, base)?;129 let net = match kind {130 "core" => three::graph::core_graph(&cell)?,131 "edge" => three::graph::edge_graph(&cell)?,132 _ => three::graph::tunnel_graph(&cell)?,133 };134 Ok((net, Some(three::census(&cell)?.euler)))135 }136 _ => {137 let cell = six::cut(&three::create(code, number, level, base)?)?;138 let mut net = match kind {139 "core" => six::graph::slice_core_graph(&cell)?,140 "dual" => six::graph::slice_dual_graph(&cell)?,141 _ => six::graph::slice_edge_graph(&cell, Some(six::FILL))?,142 };143 for node in &mut net.nodes {144 node.position[0] *= 0.5;145 node.position[1] *= ROOT3 / 4.0;146 }147 Ok((net, Some(six::fills_only(&cell).euler)))148 }149 }150}151152/// Lists the node positions of the design's graph: the dimension, the node count, then that many coordinates per node, a hex slice already at its true aspect with unit triangle sides.153#[wasm_bindgen]154pub fn graph_nodes(155 space: &str,156 code: &str,157 number: usize,158 level: usize,159 base: usize,160 kind: &str,161) -> Result<Vec<f32>, Fault> {162 let (net, _) = network(space, code, number, level, base, kind)?;163 let mut out = vec![net.dim as f32, net.nodes.len() as f32];164 for node in &net.nodes {165 out.extend(node.position.iter().map(|&p| p as f32));166 }167 Ok(out)168}169170/// Lists the branches of the design's graph as node index pairs.171#[wasm_bindgen]172pub fn graph_branches(173 space: &str,174 code: &str,175 number: usize,176 level: usize,177 base: usize,178 kind: &str,179) -> Result<Vec<u32>, Fault> {180 let (net, _) = network(space, code, number, level, base, kind)?;181 Ok(net182 .branches183 .iter()184 .flat_map(|b| [b.parent as u32, b.child as u32])185 .collect())186}187188/// Tags every node of the design's graph by degree: 0 alone, 1 a tip, 2 on a path, 3 a junction.189#[wasm_bindgen]190pub fn graph_roles(191 space: &str,192 code: &str,193 number: usize,194 level: usize,195 base: usize,196 kind: &str,197) -> Result<Vec<u8>, Fault> {198 let (net, _) = network(space, code, number, level, base, kind)?;199 Ok(roles(&net)200 .iter()201 .map(|role| match role {202 Role::Alone => 0,203 Role::Tip => 1,204 Role::Through => 2,205 Role::Junction => 3,206 })207 .collect())208}209210/// Takes the census of the design's graph: nodes, branches, tips, junctions, pieces, total length, box dimension, and the Euler number of the design the graph came from, as JSON.211#[wasm_bindgen]212pub fn graph_census(213 space: &str,214 code: &str,215 number: usize,216 level: usize,217 base: usize,218 kind: &str,219) -> Result<String, Fault> {220 let (net, euler) = network(space, code, number, level, base, kind)?;221 let tally = census(&net);222 Ok(json!({223 "dim": net.dim,224 "nodes": tally.nodes,225 "branches": tally.branches,226 "tips": tally.tips,227 "junctions": tally.junctions,228 "components": tally.components,229 "length": tally.total_length,230 "box": tally.fractal_dimension,231 "euler": euler,232 })233 .to_string())234}235236/// A force layout: the nodes push apart, the branches pull, and a cooling cap lets the lattice settle into a shape.237#[wasm_bindgen]238pub struct Layout {239 inner: Relax,240}241242#[wasm_bindgen]243impl Layout {244 /// Starts from flat positions, `dim` floats per node, and the branch pairs, jittered by the seed.245 #[wasm_bindgen(constructor)]246 pub fn new(247 positions: &[f32],248 branches: &[u32],249 dim: usize,250 seed: u32,251 ) -> Result<Layout, Fault> {252 let positions: Vec<f64> = positions.iter().map(|&p| f64::from(p)).collect();253 let pairs: Vec<(usize, usize)> = branches254 .chunks(2)255 .map(|pair| (pair[0] as usize, *pair.get(1).unwrap_or(&pair[0]) as usize))256 .collect();257 Ok(Layout {258 inner: graph::Layout::new(&positions, &pairs, dim, u64::from(seed))?,259 })260 }261 /// Runs the ticks and returns the energy left, the mean net force per node in units of the ideal length.262 pub fn step(&mut self, ticks: usize) -> f64 {263 self.inner.step(ticks)264 }265 /// Returns the positions, `dim` floats per node.266 pub fn positions(&self) -> Vec<f32> {267 self.inner.positions().iter().map(|&p| p as f32).collect()268 }269 /// Returns the energy after the last tick.270 pub fn energy(&self) -> f64 {271 self.inner.energy()272 }273 /// Returns the mean distance a node moved in the last tick.274 pub fn moved(&self) -> f64 {275 self.inner.moved()276 }277 /// Returns the ticks stepped so far.278 pub fn ticks(&self) -> u32 {279 self.inner.ticks() as u32280 }281 /// Returns the cap on one node's move in the next tick.282 pub fn temperature(&self) -> f64 {283 self.inner.temperature()284 }285}