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}