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}