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