Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-flow/src/lib.rs

//! Source-agnostic, host-clock driven flow graph. Observable state follows Facett's
//! Elm contract. Derived spatial / cluster caches are outside the serializable model.
//! GPU resources use facett-core's install, buffer, offscreen and pass-clock seams.
#[cfg(any(feature = "boxes", feature = "status", feature = "rate"))]
pub mod attr;
#[cfg(feature = "host")]
mod curve;
#[cfg(feature = "wgpu")]
pub mod gpu;
#[cfg(feature = "host")]
pub mod host;
mod scene;
#[cfg(feature = "host")]
pub use host::{
    Animation, CpuStyle, FlowPalette, FlowScene, Hit, OnScreen, PaintOpts, Painted, View, paint,
};
#[cfg(feature = "layered")]
pub use scene::LayeredOpts;
#[cfg(feature = "labels")]
mod labels;
mod view;
use facett_core::{Elm, FacetCaps};
pub use scene::{Edge, Graph, Node};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Camera {
    /// World position at the centre of the pane. Zoom is relative to fit-to-pane.
    pub centre: [f32; 2],
    pub zoom: f32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Model {
    pub id: String,
    pub title: String,
    pub graph: Arc<Graph>,
    pub camera: Camera,
    pub seconds: f64,
    pub running: bool,
    pub reduced_motion: bool,
    /// Explicit clusters; raw mode always submits every original node and edge.
    pub clustered: bool,
    pub selected: Option<String>,
    #[cfg(feature = "status")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<Arc<attr::StatusTable>>,
    #[cfg(feature = "rate")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rate: Option<Arc<attr::Rates>>,
    #[cfg(feature = "boxes")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub boxes: Option<Arc<attr::Sizes>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Msg {
    Tick(f64),
    Camera(Camera),
    Running(bool),
    ReducedMotion(bool),
    Clustered(bool),
    Select(Option<String>),
    Replace(Arc<Graph>),
    Fit,
    #[cfg(feature = "status")]
    Status(Arc<attr::StatusTable>),
    #[cfg(feature = "rate")]
    Rates(Arc<attr::Rates>),
    #[cfg(feature = "boxes")]
    Sizes(Arc<attr::Sizes>),
}
pub enum Effect {}
pub struct FlowGraph {
    model: Model,
    pub(crate) scene: scene::Scene,
    #[cfg(feature = "host")]
    pub(crate) host_scene: Option<host::FlowScene>,
    #[cfg(feature = "labels")]
    pub(crate) labels: bool,
    #[cfg(feature = "compact-state")]
    compact: bool,
    #[cfg(feature = "compact-state")]
    graph_digest: u64,
}
impl FlowGraph {
    pub fn new(id: impl Into<String>, graph: Graph) -> Result<Self, String> {
        let id = id.into();
        if id.trim().is_empty() {
            return Err("Facet ID must be nonempty".into());
        }
        graph.validate()?;
        let scene = scene::Scene::new(&graph);
        let camera = Camera {
            centre: scene.centre(),
            zoom: 1.0,
        };
        #[cfg(feature = "compact-state")]
        let graph_digest = scene::digest(&graph, 0);
        Ok(Self {
            #[cfg(feature = "host")]
            host_scene: None,
            #[cfg(feature = "labels")]
            labels: false,
            #[cfg(feature = "compact-state")]
            compact: false,
            #[cfg(feature = "compact-state")]
            graph_digest,
            model: Model {
                id,
                title: "Flow graph".into(),
                graph: Arc::new(graph),
                camera,
                seconds: 0.0,
                running: true,
                reduced_motion: false,
                clustered: false,
                selected: None,
                #[cfg(feature = "status")]
                status: None,
                #[cfg(feature = "rate")]
                rate: None,
                #[cfg(feature = "boxes")]
                boxes: None,
            },
            scene,
        })
    }
    #[allow(unused_mut)] // optional attribute setters rebuild the derived host scene
    pub fn from_model(model: Model) -> Result<Self, String> {
        model.graph.validate()?;
        if model.id.trim().is_empty()
            || !valid_camera(&model.camera)
            || !model.seconds.is_finite()
            || model.seconds < 0.0
            || model
                .selected
                .as_ref()
                .is_some_and(|id| !model.graph.nodes.iter().any(|n| &n.id == id))
        {
            return Err("Invalid flow state".into());
        }
        let scene = scene::Scene::new(&model.graph);
        #[cfg(feature = "compact-state")]
        let graph_digest = scene::digest(&model.graph, 0);
        let mut out = Self {
            model,
            scene,
            #[cfg(feature = "host")]
            host_scene: None,
            #[cfg(feature = "labels")]
            labels: false,
            #[cfg(feature = "compact-state")]
            compact: false,
            #[cfg(feature = "compact-state")]
            graph_digest,
        };
        #[cfg(feature = "status")]
        if let Some(s) = out.model.status.clone() {
            out.ensure_host()?.set_status((*s).clone())?;
        }
        #[cfg(feature = "rate")]
        if let Some(s) = out.model.rate.clone() {
            out.ensure_host()?.set_rates((*s).clone())?;
        }
        #[cfg(feature = "boxes")]
        if let Some(s) = out.model.boxes.clone() {
            out.ensure_host()?.set_sizes((*s).clone())?;
        }
        Ok(out)
    }
    /// Derived bounds for host camera scripts; does not mutate the model.
    pub fn bounds(&self) -> egui::Rect {
        #[cfg(feature = "host")]
        if let Some(scene) = &self.host_scene {
            return scene.bounds();
        }
        self.scene.bounds
    }
    /// Exact submitted geometry counts for the currently selected render mode.
    pub fn render_counts(&self) -> (usize, usize) {
        let geometry = if self.model.clustered {
            &self.scene.clusters
        } else {
            &self.scene.raw
        };
        (geometry.points.len(), geometry.links.len())
    }
    #[cfg(any(feature = "boxes", feature = "status", feature = "rate"))]
    fn ensure_host(&mut self) -> Result<&mut host::FlowScene, String> {
        if self.host_scene.is_none() {
            self.host_scene = Some(host::FlowScene::new((*self.model.graph).clone())?);
        }
        Ok(self.host_scene.as_mut().unwrap())
    }
    #[cfg(feature = "labels")]
    pub fn set_labels(&mut self, enabled: bool) {
        self.labels = enabled;
    }
    #[cfg(feature = "compact-state")]
    pub fn set_compact_state(&mut self, enabled: bool) {
        self.compact = enabled;
    }
    #[cfg(feature = "compact-state")]
    pub fn compact_state_json(&self) -> serde_json::Value {
        serde_json::json!({"node_count":self.model.graph.nodes.len(),"edge_count":self.model.graph.edges.len(),"graph_digest":self.graph_digest,"camera":self.model.camera,"seconds":self.model.seconds,"running":self.model.running,"reduced_motion":self.model.reduced_motion,"clustered":self.model.clustered,"selected":self.model.selected,"render_counts":self.render_counts(),"lane":if cfg!(feature="wgpu"){"wgpu-capable"}else{"cpu"}})
    }
    pub fn local() -> Self {
        let mut flow = Self::new("flow-hub", Graph::hub_demo(10_000)).expect("valid fixture");
        Elm::update(&mut flow, Msg::Clustered(true));
        flow
    }
}
fn valid_camera(c: &Camera) -> bool {
    c.centre.iter().all(|v| v.is_finite() && v.abs() <= 1.0e8)
        && c.zoom.is_finite()
        && (0.1..=100.0).contains(&c.zoom)
}
impl Elm for FlowGraph {
    type Model = Model;
    type Msg = Msg;
    type Effect = Effect;
    fn title(&self) -> &str {
        &self.model.title
    }
    fn state(&self) -> &Model {
        &self.model
    }
    #[allow(clippy::collapsible_match)] // Keep fallible mutating scene setters out of match guards.
    fn update(&mut self, msg: Msg) -> Vec<Effect> {
        match msg {
            Msg::Tick(dt)
                if self.model.running
                    && !self.model.reduced_motion
                    && dt.is_finite()
                    && dt >= 0.0 =>
            {
                self.model.seconds = (self.model.seconds.rem_euclid(3600.0)
                    + dt.rem_euclid(3600.0))
                .rem_euclid(3600.0);
            }
            Msg::Camera(camera) if valid_camera(&camera) => self.model.camera = camera,
            Msg::Running(value) => self.model.running = value,
            Msg::ReducedMotion(value) => self.model.reduced_motion = value,
            Msg::Clustered(value) => self.model.clustered = value,
            Msg::Select(id) if id.as_ref().is_none_or(|id| self.scene.ids.contains_key(id)) => {
                self.model.selected = id
            }
            Msg::Replace(graph) if graph.validate().is_ok() => {
                self.scene = scene::Scene::new(&graph);
                #[cfg(feature = "compact-state")]
                {
                    self.graph_digest = scene::digest(&graph, 0);
                }
                #[cfg(feature = "host")]
                {
                    self.host_scene = None;
                }
                #[cfg(feature = "status")]
                {
                    self.model.status = None;
                }
                #[cfg(feature = "rate")]
                {
                    self.model.rate = None;
                }
                #[cfg(feature = "boxes")]
                {
                    self.model.boxes = None;
                }
                if self
                    .model
                    .selected
                    .as_ref()
                    .is_some_and(|id| !self.scene.ids.contains_key(id))
                {
                    self.model.selected = None;
                }
                self.model.graph = graph;
                self.model.camera = Camera {
                    centre: self.scene.centre(),
                    zoom: 1.0,
                };
            }
            #[cfg(feature = "status")]
            Msg::Status(value) => {
                if self
                    .ensure_host()
                    .and_then(|s| s.set_status((*value).clone()))
                    .is_ok()
                {
                    self.model.status = Some(value);
                }
            }
            #[cfg(feature = "rate")]
            Msg::Rates(value) => {
                if self
                    .ensure_host()
                    .and_then(|s| s.set_rates((*value).clone()))
                    .is_ok()
                {
                    self.model.rate = Some(value);
                }
            }
            #[cfg(feature = "boxes")]
            Msg::Sizes(value) => {
                if self
                    .ensure_host()
                    .and_then(|s| s.set_sizes((*value).clone()))
                    .is_ok()
                {
                    self.model.boxes = Some(value);
                }
            }
            Msg::Fit => {
                self.model.camera = Camera {
                    centre: self.bounds().center().into(),
                    zoom: 1.0,
                }
            }
            _ => {}
        }
        Vec::new()
    }
    fn view(&self, ui: &mut egui::Ui) -> Vec<Msg> {
        facett_core::navgraph::declare_own_draw_order(ui); // atoms follow the data order (navgraph §9)
        ui.push_id(("flow-scale", &self.model.id), |ui| view::show(self, ui))
            .inner
    }
}
facett_core::impl_facet_via_elm!(FlowGraph, custom_state_json, {
    fn state_json(&self) -> serde_json::Value {
        #[cfg(feature = "compact-state")]
        if self.compact {
            return self.compact_state_json();
        }
        serde_json::to_value(&self.model).unwrap_or(serde_json::Value::Null)
    }
    fn caps(&self) -> FacetCaps {
        FacetCaps::NONE.themeable().selectable().resizable()
    }
    fn selection_json(&self) -> serde_json::Value {
        serde_json::json!(self.model.selected)
    }
    fn update_json(&mut self, json: &str) {
        if let Ok(msg) = serde_json::from_str::<Msg>(json) {
            let _ = Elm::update(self, msg);
        }
    }
});

/// Features compiled into this binary, for reproducible benchmark reports.
pub fn compiled_features() -> Vec<&'static str> {
    let mut result = Vec::new();
    if cfg!(feature = "wgpu") {
        result.push("wgpu");
    }
    if cfg!(feature = "host") {
        result.push("host");
    }
    if cfg!(feature = "labels") {
        result.push("labels");
    }
    if cfg!(feature = "boxes") {
        result.push("boxes");
    }
    if cfg!(feature = "arrows") {
        result.push("arrows");
    }
    if cfg!(feature = "status") {
        result.push("status");
    }
    if cfg!(feature = "rate") {
        result.push("rate");
    }
    if cfg!(feature = "layered") {
        result.push("layered");
    }
    if cfg!(feature = "compact-state") {
        result.push("compact-state");
    }
    if cfg!(feature = "hit-test") {
        result.push("hit-test");
    }
    if cfg!(feature = "lod") {
        result.push("lod");
    }
    result
}