use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU8, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use serde_json::{json, Value};
use uzor::core::types::Rect;
use uzor::framework::app::{App, CursorCaptureMode, NoPanel};
use uzor::framework::builder::AppBuilder;
use uzor::framework::frame_profiler::FrameProfiler;
use uzor::framework::multi_window::{WindowCtx, WindowKey, WindowSpec};
use uzor::input::{KeyCode, MouseButton, PlatformEvent};
use uzor::layout::agent::{AgentAction, AgentActionReply, AgentWidget, BlackboxAgentSurface};
use uzor::layout::{EdgeSide, EdgeSlot, LayoutManager};
use uzor::platform::types::CornerStyle;
use uzor::render::{RenderContext, RenderRegion};
use uzor_desktop::{AppRun3D as _, Scene3DApp, Scene3DFrame};
use uzor_graph::interaction::fly::{FlyController, KEYBOARD_SENSITIVITY_MAX, KEYBOARD_SENSITIVITY_MIN, MOUSE_SENSITIVITY_MAX, MOUSE_SENSITIVITY_MIN};
use uzor_graph::{
FilterSpec, Graph, GraphEngine, GraphEngine3D, GraphLayoutMode, GraphLayoutMode3D, GroupId, Layout, LayoutKind,
NodeIndex, SelectMode, TransitionDirection,
};
const AGENT_PORT: u16 = 17481;
const BLACKBOX_SLOT: &str = "graph";
const SIDEBAR_SLOT: &str = "sidebar";
const SIDEBAR_WIDTH: f32 = 320.0;
const DIMENSION_TRANSITION_MS: f64 = 600.0;
const NUM_CLUSTERS: usize = 6;
const CLUSTER_SIZE: usize = 88;
const EDGES_PER_NODE: usize = 3;
struct DetRng(u64);
impl DetRng {
fn new(seed: u64) -> Self {
Self(seed ^ 0x9E37_79B9_7F4A_7C15)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn next_f32(&mut self) -> f32 {
(self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
}
fn range_usize(&mut self, n: usize) -> usize {
if n == 0 { 0 } else { (self.next_u64() % n as u64) as usize }
}
}
type DemoGraph = Graph<(), ()>;
fn apply_degree_radius(graph: &mut DemoGraph) {
let ids: Vec<NodeIndex> = graph.nodes().map(|(id, _)| id).collect();
for id in ids {
let degree = graph.degree(id);
graph.set_radius(id, 3.0 + (degree as f32).sqrt() * 1.6);
}
}
fn build_clusters_graph() -> (DemoGraph, Vec<(f32, f32)>, Vec<Vec<NodeIndex>>) {
let mut graph = DemoGraph::new();
let mut positions = Vec::new();
let mut cluster_members: Vec<Vec<NodeIndex>> = vec![Vec::new(); NUM_CLUSTERS];
let mut hubs = Vec::with_capacity(NUM_CLUSTERS);
for cluster in 0..NUM_CLUSTERS {
let angle = cluster as f32 * 2.399_963;
let cx = angle.cos() * 420.0;
let cy = angle.sin() * 420.0;
for member in 0..CLUSTER_SIZE {
let mut rng = DetRng::new((cluster as u64) << 32 | member as u64);
let jitter_r = rng.next_f32() * 140.0;
let jitter_a = rng.next_f32() * std::f32::consts::TAU;
let x = cx + jitter_a.cos() * jitter_r;
let y = cy + jitter_a.sin() * jitter_r;
let id = graph.push_node((), format!("c{cluster}n{member}"), format!("cluster-{cluster}"), 4.0);
cluster_members[cluster].push(id);
positions.push((x, y));
}
let hub_id = graph.push_node((), format!("hub-{cluster}"), "hub", 4.0);
hubs.push(hub_id);
positions.push((cx, cy));
}
for cluster in 0..NUM_CLUSTERS {
let members = &cluster_members[cluster];
for (i, &node) in members.iter().enumerate() {
let mut rng = DetRng::new(0xC0FF_EE00 ^ ((cluster as u64) << 20) ^ i as u64);
for _ in 0..EDGES_PER_NODE {
let j = rng.range_usize(members.len());
if j != i {
graph.push_edge(node, members[j], 1.0, ());
}
}
}
for (i, &node) in members.iter().enumerate() {
if i % 6 == 0 {
graph.push_edge(hubs[cluster], node, 1.0, ());
}
}
}
for cluster in 0..NUM_CLUSTERS {
let next = (cluster + 1) % NUM_CLUSTERS;
graph.push_edge(hubs[cluster], hubs[next], 0.6, ());
}
apply_degree_radius(&mut graph);
(graph, positions, cluster_members)
}
const TREE_NODE_BUDGET: usize = 300;
const TREE_MIN_BRANCH_DEPTH: u32 = 4;
const TREE_MAX_DEPTH: u32 = 6;
fn build_tree_internal(node_budget: usize, category_prefix: &str) -> (DemoGraph, Vec<(f32, f32)>, Vec<u32>) {
let mut graph = DemoGraph::new();
let mut positions = Vec::with_capacity(node_budget);
let mut depths = Vec::with_capacity(node_budget);
let root = graph.push_node((), format!("{category_prefix}-root"), format!("{category_prefix}-0"), 6.0);
positions.push((0.0, 0.0));
depths.push(0u32);
let mut stack: Vec<(NodeIndex, u32, f32, f32)> = vec![(root, 0, 0.0, 0.0)];
let mut count = 1usize;
let mut seq = 0u64;
while let Some((parent, depth, px, py)) = stack.pop() {
if depth >= TREE_MAX_DEPTH || count >= node_budget {
continue;
}
let mut rng = DetRng::new(0xC0FF_EE01 ^ ((depth as u64) << 40) ^ seq);
seq += 1;
let children = if depth < TREE_MIN_BRANCH_DEPTH { 2 + rng.range_usize(3) } else { 1 + rng.range_usize(3) };
for c in 0..children {
if count >= node_budget {
break;
}
let angle = (c as f32 / children as f32) * std::f32::consts::TAU + depth as f32 * 0.7;
let step = 70.0 + depth as f32 * 12.0;
let x = px + angle.cos() * step;
let y = py + angle.sin() * step;
let child_depth = depth + 1;
let label = format!("{category_prefix}-{child_depth}-{count}");
let category = format!("{category_prefix}-{}", child_depth.min(6));
let child = graph.push_node((), label, category, 4.0);
graph.push_edge(parent, child, 1.0, ());
positions.push((x, y));
depths.push(child_depth);
stack.push((child, child_depth, x, y));
count += 1;
}
}
apply_degree_radius(&mut graph);
(graph, positions, depths)
}
fn build_tree_graph() -> (DemoGraph, Vec<(f32, f32)>, Vec<Vec<NodeIndex>>) {
let (graph, positions, _depths) = build_tree_internal(TREE_NODE_BUDGET, "tree");
(graph, positions, Vec::new())
}
const HIERARCHY_CROSS_LINK_RATIO: f32 = 0.05;
fn build_hierarchy_graph() -> (DemoGraph, Vec<(f32, f32)>, Vec<Vec<NodeIndex>>) {
let (mut graph, positions, depths) = build_tree_internal(TREE_NODE_BUDGET, "hier");
let n = graph.node_count();
let mut by_depth: std::collections::BTreeMap<u32, Vec<NodeIndex>> = std::collections::BTreeMap::new();
for (i, &d) in depths.iter().enumerate() {
by_depth.entry(d).or_default().push(NodeIndex(i as u32));
}
for i in 0..n {
let mut rng = DetRng::new(0xDEC0_DE55 ^ i as u64);
if rng.next_f32() >= HIERARCHY_CROSS_LINK_RATIO {
continue;
}
let depth = depths[i];
let candidates = by_depth.get(&depth).map(Vec::as_slice).unwrap_or(&[]);
if candidates.len() < 2 {
continue;
}
let j = candidates[rng.range_usize(candidates.len())];
if j.index() != i {
graph.push_edge(NodeIndex(i as u32), j, 0.6, ());
}
}
(graph, positions, Vec::new())
}
const SPARSE_NODE_COUNT: usize = 360;
const SPARSE_LONG_RANGE_EDGE_PROBABILITY: f32 = 0.55;
fn build_sparse_graph() -> (DemoGraph, Vec<(f32, f32)>, Vec<Vec<NodeIndex>>) {
let mut graph = DemoGraph::new();
let mut positions = Vec::with_capacity(SPARSE_NODE_COUNT);
let mut ids = Vec::with_capacity(SPARSE_NODE_COUNT);
for i in 0..SPARSE_NODE_COUNT {
let mut rng = DetRng::new(0xBADC_0FFE ^ i as u64);
let r = rng.next_f32().sqrt() * 900.0;
let a = rng.next_f32() * std::f32::consts::TAU;
let x = a.cos() * r;
let y = a.sin() * r;
let category = format!("sparse-{}", i % 8);
let id = graph.push_node((), format!("s{i}"), category, 3.5);
ids.push(id);
positions.push((x, y));
}
for i in 0..SPARSE_NODE_COUNT {
let mut rng = DetRng::new(0xFACE_B00C ^ ((i as u64) << 16));
if rng.next_f32() >= SPARSE_LONG_RANGE_EDGE_PROBABILITY {
continue;
}
let j = rng.range_usize(SPARSE_NODE_COUNT);
if j != i {
graph.push_edge(ids[i], ids[j], 1.0, ());
}
}
apply_degree_radius(&mut graph);
(graph, positions, Vec::new())
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Fixture {
Clusters,
Tree,
Hierarchy,
Sparse,
}
impl Fixture {
fn as_str(self) -> &'static str {
match self {
Fixture::Clusters => "clusters",
Fixture::Tree => "tree",
Fixture::Hierarchy => "hierarchy",
Fixture::Sparse => "sparse",
}
}
fn from_str(s: &str) -> Option<Self> {
match s {
"clusters" => Some(Fixture::Clusters),
"tree" => Some(Fixture::Tree),
"hierarchy" => Some(Fixture::Hierarchy),
"sparse" => Some(Fixture::Sparse),
_ => None,
}
}
fn code(self) -> u8 {
match self {
Fixture::Clusters => 0,
Fixture::Tree => 1,
Fixture::Hierarchy => 2,
Fixture::Sparse => 3,
}
}
fn from_code(code: u8) -> Self {
match code {
1 => Fixture::Tree,
2 => Fixture::Hierarchy,
3 => Fixture::Sparse,
_ => Fixture::Clusters,
}
}
}
fn build_fixture(fixture: Fixture) -> (DemoGraph, Vec<(f32, f32)>, Vec<Vec<NodeIndex>>) {
match fixture {
Fixture::Clusters => build_clusters_graph(),
Fixture::Tree => build_tree_graph(),
Fixture::Hierarchy => build_hierarchy_graph(),
Fixture::Sparse => build_sparse_graph(),
}
}
#[derive(Clone)]
struct FixtureState(Arc<AtomicU8>);
impl FixtureState {
fn new() -> Self {
Self(Arc::new(AtomicU8::new(Fixture::Clusters.code())))
}
fn get(&self) -> Fixture {
Fixture::from_code(self.0.load(Ordering::Relaxed))
}
fn set(&self, fixture: Fixture) {
self.0.store(fixture.code(), Ordering::Relaxed);
}
}
type Engine = GraphEngine<(), (), GraphLayoutMode>;
type Engine3D = GraphEngine3D<(), (), GraphLayoutMode3D>;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Dimension {
TwoD,
ThreeD,
}
impl Dimension {
fn from_code(code: u8) -> Self {
if code == 3 { Dimension::ThreeD } else { Dimension::TwoD }
}
fn code(self) -> u8 {
match self {
Dimension::TwoD => 2,
Dimension::ThreeD => 3,
}
}
}
#[derive(Clone)]
struct DimState(Arc<AtomicU8>);
impl DimState {
fn new() -> Self {
Self(Arc::new(AtomicU8::new(Dimension::TwoD.code())))
}
fn get(&self) -> Dimension {
Dimension::from_code(self.0.load(Ordering::Relaxed))
}
fn set(&self, dim: Dimension) {
self.0.store(dim.code(), Ordering::Relaxed);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum NavMode {
Orbit,
Fly,
}
impl NavMode {
fn as_str(self) -> &'static str {
match self {
NavMode::Orbit => "orbit",
NavMode::Fly => "fly",
}
}
fn from_str(s: &str) -> Option<Self> {
match s {
"orbit" => Some(NavMode::Orbit),
"fly" => Some(NavMode::Fly),
_ => None,
}
}
fn code(self) -> u8 {
match self {
NavMode::Orbit => 0,
NavMode::Fly => 1,
}
}
fn from_code(code: u8) -> Self {
if code == 1 { NavMode::Fly } else { NavMode::Orbit }
}
}
#[derive(Clone)]
struct NavModeState(Arc<AtomicU8>);
impl NavModeState {
fn new() -> Self {
Self(Arc::new(AtomicU8::new(NavMode::Orbit.code())))
}
fn get(&self) -> NavMode {
NavMode::from_code(self.0.load(Ordering::Relaxed))
}
fn set(&self, mode: NavMode) {
self.0.store(mode.code(), Ordering::Relaxed);
}
}
#[derive(Clone)]
struct MouseLookState(Arc<AtomicBool>);
impl MouseLookState {
fn new() -> Self {
Self(Arc::new(AtomicBool::new(true)))
}
fn get(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
fn set(&self, on: bool) {
self.0.store(on, Ordering::Relaxed);
}
fn toggle(&self) -> bool {
let next = !self.get();
self.set(next);
next
}
}
#[derive(Clone)]
struct HudVisibleState(Arc<AtomicBool>);
impl HudVisibleState {
fn new() -> Self {
Self(Arc::new(AtomicBool::new(true)))
}
fn get(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
fn set(&self, on: bool) {
self.0.store(on, Ordering::Relaxed);
}
fn toggle(&self) -> bool {
let next = !self.get();
self.set(next);
next
}
}
fn full_window_viewport() -> Rect {
Rect::new(-1.0e9, -1.0e9, 2.0e9, 2.0e9)
}
fn viewport_from_surface_size(size: Option<(u32, u32)>) -> Rect {
match size {
Some((w, h)) => Rect::new(0.0, 0.0, w as f64, h as f64),
None => full_window_viewport(),
}
}
#[derive(Clone)]
struct SurfaceSizeState(Arc<(AtomicU32, AtomicU32)>);
impl SurfaceSizeState {
fn new() -> Self {
Self(Arc::new((AtomicU32::new(0), AtomicU32::new(0))))
}
fn set(&self, w: u32, h: u32) {
self.0 .0.store(w, Ordering::Relaxed);
self.0 .1.store(h, Ordering::Relaxed);
}
fn get(&self) -> Option<(u32, u32)> {
let w = self.0 .0.load(Ordering::Relaxed);
let h = self.0 .1.load(Ordering::Relaxed);
if w == 0 || h == 0 {
None
} else {
Some((w, h))
}
}
}
fn draw_fly_crosshair(ctx: &mut dyn RenderContext, viewport: Rect) {
const ARM: f64 = 9.0;
const GAP: f64 = 4.0;
const THICK: f64 = 1.5;
let cx = viewport.x + viewport.width / 2.0;
let cy = viewport.y + viewport.height / 2.0;
ctx.set_global_alpha(0.85);
ctx.set_fill_color("#e6e6ea");
ctx.fill_rect(cx - GAP - ARM, cy - THICK / 2.0, ARM, THICK);
ctx.fill_rect(cx + GAP, cy - THICK / 2.0, ARM, THICK);
ctx.fill_rect(cx - THICK / 2.0, cy - GAP - ARM, THICK, ARM);
ctx.fill_rect(cx - THICK / 2.0, cy + GAP, THICK, ARM);
ctx.fill_rect(cx - 1.0, cy - 1.0, 2.0, 2.0);
ctx.set_global_alpha(1.0);
}
const HUD_PAD: f64 = 12.0;
const HUD_MARGIN: f64 = 12.0;
const HUD_HEADING_H: f64 = 16.0;
const HUD_BUTTON_H: f64 = 27.0;
const HUD_BUTTON_GAP: f64 = 5.0;
const HUD_SECTION_GAP: f64 = 10.0;
const HUD_SLIDER_ROW_H: f64 = 32.0;
const HUD_TEXT_LINE_H: f64 = 16.0;
const HUD_STATUS_LINE_COUNT: usize = 2;
const FALLBACK_SURFACE_WIDTH_LOGICAL: f64 = 1400.0;
fn surface_width_logical(size: Option<(u32, u32)>, scale_factor: f64) -> f64 {
match size {
Some((w, _)) if scale_factor > 0.0 => w as f64 / scale_factor,
_ => FALLBACK_SURFACE_WIDTH_LOGICAL,
}
}
const FALLBACK_SURFACE_HEIGHT_LOGICAL: f64 = 900.0;
fn surface_height_logical(size: Option<(u32, u32)>, scale_factor: f64) -> f64 {
match size {
Some((_, h)) if scale_factor > 0.0 => h as f64 / scale_factor,
_ => FALLBACK_SURFACE_HEIGHT_LOGICAL,
}
}
fn hud_card_y(viewport_h: f64, card_h: f64) -> f64 {
((viewport_h - card_h) / 2.0).max(HUD_MARGIN)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum HudControl {
Fixture(Fixture),
SetLayout(LayoutKind),
ToggleDimension,
ToggleNavMode,
FitView,
ToggleGrid,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum HudSliderId {
Keyboard,
Mouse,
}
struct HudButtonRect {
control: HudControl,
rect: Rect,
label: String,
active: bool,
paused: bool,
}
struct HudSliderRect {
id: HudSliderId,
track: Rect,
hit: Rect,
label: &'static str,
value: f32,
min: f32,
max: f32,
}
struct HudLayout {
panel: Rect,
headings: Vec<(&'static str, f64)>,
buttons: Vec<HudButtonRect>,
sliders: Vec<HudSliderRect>,
legend: Vec<(&'static str, &'static str, f64)>,
status_y: f64,
selection_y: f64,
}
#[derive(Clone, Copy)]
struct HudSnapshot {
dim: Dimension,
fixture: Fixture,
nav_mode: NavMode,
grid_enabled: bool,
keyboard_sensitivity: f32,
mouse_sensitivity: f32,
layout_kind: LayoutKind,
layout_paused: bool,
selection_line_count: usize,
}
fn build_hud_layout(origin_x: f64, origin_y: f64, width: f64, snap: &HudSnapshot) -> HudLayout {
let content_w = (width - 2.0 * HUD_PAD).max(0.0);
let mut y = origin_y + HUD_PAD;
let mut headings: Vec<(&'static str, f64)> = Vec::new();
let mut buttons: Vec<HudButtonRect> = Vec::new();
let mut sliders: Vec<HudSliderRect> = Vec::new();
headings.push(("FIXTURE", y));
y += HUD_HEADING_H;
for fixture in [Fixture::Clusters, Fixture::Tree, Fixture::Hierarchy, Fixture::Sparse] {
let rect = Rect::new(origin_x + HUD_PAD, y, content_w, HUD_BUTTON_H);
buttons.push(HudButtonRect {
control: HudControl::Fixture(fixture),
rect,
label: fixture.as_str().to_ascii_uppercase(),
active: fixture == snap.fixture,
paused: false,
});
y += HUD_BUTTON_H + HUD_BUTTON_GAP;
}
y += HUD_SECTION_GAP;
headings.push(("LAYOUT", y));
y += HUD_HEADING_H;
for &kind in available_layout_kinds(snap.fixture) {
let rect = Rect::new(origin_x + HUD_PAD, y, content_w, HUD_BUTTON_H);
let active = kind == snap.layout_kind;
let paused = active && kind == LayoutKind::Force && snap.layout_paused;
let base = layout_kind_label(kind);
let label = if paused { format!("{base} (PAUSED)") } else { base.to_owned() };
buttons.push(HudButtonRect { control: HudControl::SetLayout(kind), rect, label, active, paused });
y += HUD_BUTTON_H + HUD_BUTTON_GAP;
}
y += HUD_SECTION_GAP;
headings.push(("NAVIGATION", y));
y += HUD_HEADING_H;
{
let rect = Rect::new(origin_x + HUD_PAD, y, content_w, HUD_BUTTON_H);
buttons.push(HudButtonRect {
control: HudControl::ToggleDimension,
rect,
label: "2D / 3D — TAB".to_owned(),
active: snap.dim == Dimension::ThreeD,
paused: false,
});
y += HUD_BUTTON_H + HUD_BUTTON_GAP;
}
if snap.dim == Dimension::ThreeD {
let rect = Rect::new(origin_x + HUD_PAD, y, content_w, HUD_BUTTON_H);
buttons.push(HudButtonRect {
control: HudControl::ToggleNavMode,
rect,
label: "ORBIT / FLY — V".to_owned(),
active: snap.nav_mode == NavMode::Fly,
paused: false,
});
y += HUD_BUTTON_H + HUD_BUTTON_GAP;
}
{
let rect = Rect::new(origin_x + HUD_PAD, y, content_w, HUD_BUTTON_H);
buttons.push(HudButtonRect {
control: HudControl::FitView,
rect,
label: "FIT — HOME / F".to_owned(),
active: false,
paused: false,
});
y += HUD_BUTTON_H + HUD_BUTTON_GAP;
}
if snap.dim == Dimension::ThreeD {
let rect = Rect::new(origin_x + HUD_PAD, y, content_w, HUD_BUTTON_H);
buttons.push(HudButtonRect {
control: HudControl::ToggleGrid,
rect,
label: "GRID — G".to_owned(),
active: snap.grid_enabled,
paused: false,
});
y += HUD_BUTTON_H + HUD_BUTTON_GAP;
}
y += HUD_SECTION_GAP;
if snap.dim == Dimension::ThreeD && snap.nav_mode == NavMode::Fly {
headings.push(("SENSITIVITY", y));
y += HUD_HEADING_H;
for (id, label, value, min, max) in [
(HudSliderId::Keyboard, "KEYBOARD SPEED", snap.keyboard_sensitivity, KEYBOARD_SENSITIVITY_MIN, KEYBOARD_SENSITIVITY_MAX),
(HudSliderId::Mouse, "MOUSE LOOK", snap.mouse_sensitivity, MOUSE_SENSITIVITY_MIN, MOUSE_SENSITIVITY_MAX),
] {
let track = Rect::new(origin_x + HUD_PAD, y + 14.0, content_w, 4.0);
let hit = Rect::new(track.x, track.y - 9.0, track.width, 18.0);
sliders.push(HudSliderRect { id, track, hit, label, value, min, max });
y += HUD_SLIDER_ROW_H;
}
y += HUD_SECTION_GAP;
}
headings.push(("MOUSE", y));
y += HUD_HEADING_H;
let legend_pairs: &[(&str, &str)] = match (snap.dim, snap.nav_mode) {
(Dimension::TwoD, _) => &[("DRAG", "PAN / MOVE NODE"), ("WHEEL", "ZOOM"), ("SHIFT+DRAG", "BOX SELECT")],
(Dimension::ThreeD, NavMode::Orbit) => {
&[("LMB DRAG", "ORBIT / MOVE NODE"), ("MMB DRAG", "PAN"), ("WHEEL", "DOLLY"), ("SHIFT+DRAG", "BOX SELECT")]
}
(Dimension::ThreeD, NavMode::Fly) => &[("WASD", "MOVE"), ("MOUSE", "LOOK"), ("MMB CLICK", "CURSOR / LOOK")],
};
let mut legend = Vec::with_capacity(legend_pairs.len());
for &(input, action) in legend_pairs {
legend.push((input, action, y));
y += HUD_TEXT_LINE_H;
}
y += HUD_SECTION_GAP;
headings.push(("STATUS", y));
y += HUD_HEADING_H;
let status_y = y;
y += HUD_STATUS_LINE_COUNT as f64 * HUD_TEXT_LINE_H;
y += HUD_SECTION_GAP;
headings.push(("SELECTION", y));
y += HUD_HEADING_H;
let selection_y = y;
y += snap.selection_line_count as f64 * HUD_TEXT_LINE_H;
y += HUD_PAD;
HudLayout { panel: Rect::new(origin_x, origin_y, width, y - origin_y), headings, buttons, sliders, legend, status_y, selection_y }
}
fn hit_button(layout: &HudLayout, x: f64, y: f64) -> Option<HudControl> {
layout.buttons.iter().find(|b| b.rect.contains(x, y)).map(|b| b.control)
}
fn hit_slider(layout: &HudLayout, x: f64, y: f64) -> Option<HudSliderId> {
layout.sliders.iter().find(|s| s.hit.contains(x, y)).map(|s| s.id)
}
fn scaled_rect(r: Rect, scale: f64) -> Rect {
Rect::new(r.x * scale, r.y * scale, r.width * scale, r.height * scale)
}
fn draw_hud_static(ctx: &mut dyn RenderContext, layout: &HudLayout, scale: f64) {
let panel = scaled_rect(layout.panel, scale);
ctx.save();
ctx.set_global_alpha(0.96);
ctx.set_fill_color("#0b1019");
ctx.fill_rect(panel.x, panel.y, panel.width, panel.height);
ctx.set_global_alpha(1.0);
ctx.set_stroke_color("#2e3a4d");
ctx.set_stroke_width(1.0);
ctx.stroke_rect(panel.x, panel.y, panel.width, panel.height);
for &(label, y) in &layout.headings {
ctx.set_font("bold 10px sans-serif");
ctx.set_fill_color("#687b96");
ctx.fill_text(label, panel.x + HUD_PAD * scale, (y + 11.0) * scale);
}
for b in &layout.buttons {
let r = scaled_rect(b.rect, scale);
let (fill, stroke, text) = if b.paused {
("#3c3319", "#dba85c", "#e7c98b")
} else if b.active {
("#193c31", "#75dba0", "#8be7b2")
} else {
("#151d2a", "#36445a", "#b7c2d4")
};
ctx.set_fill_color(fill);
ctx.fill_rect(r.x, r.y, r.width, r.height);
ctx.set_stroke_color(stroke);
ctx.set_stroke_width(1.0);
ctx.stroke_rect(r.x, r.y, r.width, r.height);
ctx.set_font("bold 10px sans-serif");
ctx.set_fill_color(text);
ctx.fill_text(&b.label, r.x + 8.0 * scale, r.y + r.height * 0.65);
}
for s in &layout.sliders {
let track = scaled_rect(s.track, scale);
let normalized = (((s.value - s.min) / (s.max - s.min)) as f64).clamp(0.0, 1.0);
let knob_x = track.x + track.width * normalized;
ctx.set_font("bold 9px sans-serif");
ctx.set_fill_color("#b7c2d4");
ctx.fill_text(s.label, track.x, track.y - 8.0 * scale);
ctx.set_fill_color("#8be7b2");
ctx.fill_text(&format!("{:.2}x", s.value), track.x + track.width - 32.0 * scale, track.y - 8.0 * scale);
ctx.set_fill_color("#263246");
ctx.fill_rect(track.x, track.y, track.width, track.height);
ctx.set_fill_color("#4d90fe");
ctx.fill_rect(track.x, track.y, knob_x - track.x, track.height);
ctx.begin_path();
ctx.arc(knob_x, track.y + track.height / 2.0, 4.0 * scale, 0.0, std::f64::consts::TAU);
ctx.set_fill_color("#edf2fa");
ctx.fill();
}
ctx.set_font("bold 10px sans-serif");
for &(input, action, y) in &layout.legend {
ctx.set_fill_color("#b7c2d4");
ctx.fill_text(input, panel.x + HUD_PAD * scale, (y + 11.0) * scale);
ctx.set_font("10px sans-serif");
ctx.set_fill_color("#73839b");
ctx.fill_text(action, panel.x + 90.0 * scale, (y + 11.0) * scale);
ctx.set_font("bold 10px sans-serif");
}
ctx.restore();
}
fn draw_hud_status(ctx: &mut dyn RenderContext, panel_x: f64, status_y: f64, lines: &[String], scale: f64) {
ctx.save();
ctx.set_font("11px sans-serif");
ctx.set_fill_color("#aebbd0");
let x = (panel_x + HUD_PAD) * scale;
for (i, line) in lines.iter().enumerate() {
let y = (status_y + i as f64 * HUD_TEXT_LINE_H + 11.0) * scale;
ctx.fill_text(line, x, y);
}
ctx.restore();
}
fn selection_lines_2d(engine: &Engine) -> Vec<String> {
match engine.selected_facts() {
Some(f) => vec![
format!("selected: {}", f.label),
format!("category: {}", f.category),
format!("degree: {}", f.degree),
format!("pos: ({:.1}, {:.1})", f.position.0, f.position.1),
format!("pinned: {}", f.pinned),
],
None => vec!["no selection — click a node".to_owned()],
}
}
fn selection_lines_3d(engine3d: &Engine3D) -> Vec<String> {
match engine3d.selected().and_then(|id| engine3d.node_facts(id)) {
Some(f) => vec![
format!("selected: {}", f.label),
format!("category: {}", f.category),
format!("degree: {}", f.degree),
format!("pos: ({:.1}, {:.1})", f.position.0, f.position.1),
format!("pinned: {}", f.pinned),
],
None => vec!["no selection — click a node".to_owned()],
}
}
#[derive(Clone)]
struct CameraFitFlag(Arc<AtomicBool>);
impl CameraFitFlag {
fn new() -> Self {
Self(Arc::new(AtomicBool::new(true)))
}
fn needs_fit(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
fn request(&self) {
self.0.store(true, Ordering::Relaxed);
}
fn clear(&self) {
self.0.store(false, Ordering::Relaxed);
}
}
#[derive(Clone)]
struct FlattenPendingFlag(Arc<AtomicBool>);
impl FlattenPendingFlag {
fn new() -> Self {
Self(Arc::new(AtomicBool::new(false)))
}
fn is_pending(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
fn request(&self) {
self.0.store(true, Ordering::Relaxed);
}
fn clear(&self) {
self.0.store(false, Ordering::Relaxed);
}
}
fn default_layout_kind_for_fixture(fixture: Fixture) -> LayoutKind {
match fixture {
Fixture::Clusters | Fixture::Sparse => LayoutKind::Force,
Fixture::Tree | Fixture::Hierarchy => LayoutKind::Hierarchical,
}
}
fn available_layout_kinds(fixture: Fixture) -> &'static [LayoutKind] {
match fixture {
Fixture::Clusters | Fixture::Sparse => &[LayoutKind::Force],
Fixture::Tree | Fixture::Hierarchy => &[LayoutKind::Force, LayoutKind::Hierarchical, LayoutKind::Radial],
}
}
fn layout_kind_label(kind: LayoutKind) -> &'static str {
match kind {
LayoutKind::Force => "FORCE",
LayoutKind::Hierarchical => "LAYERED",
LayoutKind::Radial => "RADIAL",
}
}
fn rebuild_engines(engine: &Arc<Mutex<Engine>>, engine3d: &Arc<Mutex<Engine3D>>, fixture: Fixture, camera_fit: &CameraFitFlag) {
let (graph, positions, cluster_members) = build_fixture(fixture);
let mut new_engine = Engine::new(graph, GraphLayoutMode::default());
new_engine.seed_positions(&positions);
new_engine.set_agent_slot_id(BLACKBOX_SLOT);
new_engine.layout.set_kind(default_layout_kind_for_fixture(fixture));
for members in cluster_members.iter().take(COLLAPSIBLE_CLUSTERS) {
new_engine.define_cluster(members[..COLLAPSIBLE_CLUSTER_SIZE.min(members.len())].to_vec());
}
let (graph3d, positions3d, cluster_members3d) = build_fixture(fixture);
let mut new_engine3d = Engine3D::new(graph3d, GraphLayoutMode3D::default());
new_engine3d.seed_positions(&positions3d);
new_engine3d.layout.set_kind(default_layout_kind_for_fixture(fixture));
for members in cluster_members3d.iter().take(COLLAPSIBLE_CLUSTERS) {
new_engine3d.define_cluster(members[..COLLAPSIBLE_CLUSTER_SIZE.min(members.len())].to_vec());
}
*DemoApp::lock(engine) = new_engine;
*DemoApp::lock3d(engine3d) = new_engine3d;
camera_fit.request();
}
fn flatten_3d_into_2d(engine: &Arc<Mutex<Engine>>, engine3d: &Arc<Mutex<Engine3D>>, dim: &DimState) {
let positions: Vec<(f32, f32)> = {
let engine3d = DemoApp::lock3d(engine3d);
engine3d.particles.iter().map(|p| (p.x, p.y)).collect()
};
DemoApp::lock(engine).seed_positions(&positions);
dim.set(Dimension::TwoD);
}
fn apply_dimension_3d_transition(
engine: &Arc<Mutex<Engine>>,
engine3d: &Arc<Mutex<Engine3D>>,
dim: &DimState,
flatten_pending: &FlattenPendingFlag,
animate: bool,
) {
if dim.get() == Dimension::TwoD {
let positions: Vec<(f32, f32)> = {
let engine = DemoApp::lock(engine);
engine.particles.iter().map(|p| (p.x, p.y)).collect()
};
DemoApp::lock3d(engine3d).seed_positions(&positions);
dim.set(Dimension::ThreeD);
}
flatten_pending.clear();
if animate {
DemoApp::lock3d(engine3d).start_transition(TransitionDirection::In, DIMENSION_TRANSITION_MS);
}
}
fn apply_dimension_2d_transition(
engine: &Arc<Mutex<Engine>>,
engine3d: &Arc<Mutex<Engine3D>>,
dim: &DimState,
flatten_pending: &FlattenPendingFlag,
animate: bool,
) {
if dim.get() == Dimension::ThreeD {
if animate {
flatten_pending.request();
DemoApp::lock3d(engine3d).start_transition(TransitionDirection::Out, DIMENSION_TRANSITION_MS);
} else {
flatten_3d_into_2d(engine, engine3d, dim);
flatten_pending.clear();
}
}
}
struct DemoApp {
engine: Arc<Mutex<Engine>>,
engine3d: Arc<Mutex<Engine3D>>,
dim: DimState,
fixture: FixtureState,
camera_fit: CameraFitFlag,
fit3d_pending: CameraFitFlag,
nav_mode: NavModeState,
mouse_look: MouseLookState,
fly: Arc<Mutex<FlyController>>,
frame_profiler: Arc<Mutex<FrameProfiler>>,
last_3d_frame_at: Option<std::time::Instant>,
last_3d_surface_px: SurfaceSizeState,
flatten_pending: FlattenPendingFlag,
hud_visible: HudVisibleState,
scale_factor: f64,
last_sidebar_body: Rect,
hud_pressed_button: Option<HudControl>,
hud_dragging_slider: Option<HudSliderId>,
}
struct DemoBlackbox {
engine: Arc<Mutex<Engine>>,
engine3d: Arc<Mutex<Engine3D>>,
dim: DimState,
fixture: FixtureState,
camera_fit: CameraFitFlag,
fit3d_pending: CameraFitFlag,
nav_mode: NavModeState,
mouse_look: MouseLookState,
fly: Arc<Mutex<FlyController>>,
frame_profiler: Arc<Mutex<FrameProfiler>>,
surface_size: SurfaceSizeState,
flatten_pending: FlattenPendingFlag,
hud_visible: HudVisibleState,
}
fn resolve_node_3d(engine3d: &Engine3D, action: &AgentAction) -> Option<NodeIndex> {
if let Some(idx) = action.args.get("index").and_then(Value::as_u64) {
let node = NodeIndex(idx as u32);
return (node.index() < engine3d.graph.node_count()).then_some(node);
}
if let Some(label) = action.args.get("label").and_then(Value::as_str) {
return engine3d.graph.find_by_label(label);
}
None
}
fn node_facts_json_3d(engine3d: &Engine3D, id: NodeIndex) -> Option<Value> {
engine3d.node_facts(id).map(|f| {
json!({
"index": f.index.index(),
"label": f.label,
"category": f.category,
"degree": f.degree,
"position": { "x": f.position.0, "y": f.position.1 },
"pinned": f.pinned,
})
})
}
fn resolve_cluster_3d(action: &AgentAction) -> Option<GroupId> {
action.args.get("cluster").and_then(Value::as_u64).map(|v| GroupId(v as u32))
}
fn parse_select_mode_3d(action: &AgentAction) -> Result<SelectMode, String> {
match action.args.get("mode").and_then(Value::as_str) {
None | Some("replace") => Ok(SelectMode::Replace),
Some("union") => Ok(SelectMode::Union),
Some("diff") => Ok(SelectMode::Diff),
Some(other) => Err(format!("unknown select mode {other:?} (expected \"replace\"|\"union\"|\"diff\")")),
}
}
fn filter_json_3d(filter: &FilterSpec) -> Value {
json!({
"label_substring": filter.label_substring,
"categories": filter.categories,
"min_degree": filter.min_degree,
})
}
fn layout_kind_str(kind: LayoutKind) -> &'static str {
match kind {
LayoutKind::Force => "force",
LayoutKind::Hierarchical => "hierarchical",
LayoutKind::Radial => "radial",
}
}
fn selection_json_3d(engine3d: &Engine3D) -> Value {
const MAX_REPORTED_INDICES: usize = 50;
let indices: Vec<u32> = engine3d.selection.iter().take(MAX_REPORTED_INDICES).map(|n| n.0).collect();
json!({
"count": engine3d.selection.len(),
"indices": indices,
"collapsed_group": engine3d.selection_collapsed_group().map(|id| id.0),
})
}
const COLLAPSIBLE_CLUSTERS: usize = 3;
const COLLAPSIBLE_CLUSTER_SIZE: usize = 8;
impl DemoApp {
fn new() -> Self {
let engine = Arc::new(Mutex::new(GraphEngine::new(DemoGraph::new(), GraphLayoutMode::default())));
let engine3d = Arc::new(Mutex::new(Engine3D::new(DemoGraph::new(), GraphLayoutMode3D::default())));
let camera_fit = CameraFitFlag::new();
let fixture = FixtureState::new();
rebuild_engines(&engine, &engine3d, fixture.get(), &camera_fit);
Self {
engine,
engine3d,
dim: DimState::new(),
fixture,
camera_fit,
fit3d_pending: CameraFitFlag::new(),
nav_mode: NavModeState::new(),
mouse_look: MouseLookState::new(),
fly: Arc::new(Mutex::new(FlyController::new())),
frame_profiler: Arc::new(Mutex::new(FrameProfiler::default())),
last_3d_frame_at: None,
last_3d_surface_px: SurfaceSizeState::new(),
flatten_pending: FlattenPendingFlag::new(),
hud_visible: HudVisibleState::new(),
scale_factor: 1.0,
last_sidebar_body: Rect::new(0.0, 0.0, 0.0, 0.0),
hud_pressed_button: None,
hud_dragging_slider: None,
}
}
fn lock(engine: &Arc<Mutex<Engine>>) -> MutexGuard<'_, Engine> {
match engine.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
}
}
fn lock3d(engine3d: &Arc<Mutex<Engine3D>>) -> MutexGuard<'_, Engine3D> {
match engine3d.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
}
}
fn lock_fly(fly: &Arc<Mutex<FlyController>>) -> MutexGuard<'_, FlyController> {
match fly.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
}
}
fn lock_profiler(profiler: &Arc<Mutex<FrameProfiler>>) -> MutexGuard<'_, FrameProfiler> {
match profiler.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
}
}
fn toggle_nav_mode(&mut self) {
let next = match self.nav_mode.get() {
NavMode::Orbit => NavMode::Fly,
NavMode::Fly => NavMode::Orbit,
};
self.nav_mode.set(next);
self.mouse_look.set(true);
Self::lock_fly(&self.fly).stop();
}
fn on_event_fly(&mut self, event: &PlatformEvent) -> bool {
match event {
PlatformEvent::KeyDown { key, .. } => {
if Self::lock_fly(&self.fly).set_key(*key, true) {
return true;
}
}
PlatformEvent::KeyUp { key, .. } => {
if Self::lock_fly(&self.fly).set_key(*key, false) {
return true;
}
}
PlatformEvent::PointerDown { button: MouseButton::Middle, .. } => {
self.mouse_look.toggle();
return true;
}
PlatformEvent::PointerUp { button: MouseButton::Middle, .. } => {
return true;
}
PlatformEvent::PointerDelta { dx, dy } => {
if self.mouse_look.get() {
let mut engine3d = Self::lock3d(&self.engine3d);
Self::lock_fly(&self.fly).apply_look_delta(*dx as f32, *dy as f32, &mut engine3d.camera);
}
return true;
}
PlatformEvent::WindowFocused(false) => {
Self::lock_fly(&self.fly).stop();
return false;
}
_ => {}
}
let mut engine3d = Self::lock3d(&self.engine3d);
engine3d.on_event(event, self.dim3d_viewport())
}
fn dim3d_viewport(&self) -> Rect {
viewport_from_surface_size(self.last_3d_surface_px.get())
}
fn fit_view_3d(&self) {
self.fit3d_pending.request();
}
fn toggle_grid(&self) {
let mut engine3d = Self::lock3d(&self.engine3d);
let next = !engine3d.grid_enabled();
engine3d.set_grid_enabled(next);
}
fn toggle_dimension(&mut self) {
match self.dim.get() {
Dimension::TwoD => apply_dimension_3d_transition(&self.engine, &self.engine3d, &self.dim, &self.flatten_pending, true),
Dimension::ThreeD => apply_dimension_2d_transition(&self.engine, &self.engine3d, &self.dim, &self.flatten_pending, true),
}
}
fn fit_view_current_dimension(&mut self) {
match self.dim.get() {
Dimension::TwoD => self.camera_fit.request(),
Dimension::ThreeD => self.fit_view_3d(),
}
}
fn hud_snapshot(&self) -> HudSnapshot {
let grid_enabled = Self::lock3d(&self.engine3d).grid_enabled();
let (keyboard_sensitivity, mouse_sensitivity) = {
let fly = Self::lock_fly(&self.fly);
(fly.keyboard_sensitivity(), fly.mouse_sensitivity())
};
let (layout_kind, layout_paused, selection_line_count) = match self.dim.get() {
Dimension::TwoD => {
let engine = Self::lock(&self.engine);
(engine.layout.kind(), engine.layout.paused(), selection_lines_2d(&engine).len())
}
Dimension::ThreeD => {
let engine3d = Self::lock3d(&self.engine3d);
(engine3d.layout.kind(), engine3d.layout.paused(), selection_lines_3d(&engine3d).len())
}
};
HudSnapshot {
dim: self.dim.get(),
fixture: self.fixture.get(),
nav_mode: self.nav_mode.get(),
grid_enabled,
keyboard_sensitivity,
mouse_sensitivity,
layout_kind,
layout_paused,
selection_line_count,
}
}
fn hud_origin(&self, snap: &HudSnapshot) -> (f64, f64, f64) {
match self.dim.get() {
Dimension::ThreeD => {
let viewport_w = surface_width_logical(self.last_3d_surface_px.get(), self.scale_factor);
let viewport_h = surface_height_logical(self.last_3d_surface_px.get(), self.scale_factor);
let card_h = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, snap).panel.height;
(viewport_w - HUD_MARGIN - SIDEBAR_WIDTH as f64, hud_card_y(viewport_h, card_h), SIDEBAR_WIDTH as f64)
}
Dimension::TwoD => (self.last_sidebar_body.x, self.last_sidebar_body.y, self.last_sidebar_body.width),
}
}
fn hud_layout(&self) -> HudLayout {
let snap = self.hud_snapshot();
let (origin_x, origin_y, width) = self.hud_origin(&snap);
build_hud_layout(origin_x, origin_y, width, &snap)
}
fn on_event_hud(&mut self, event: &PlatformEvent) -> Option<bool> {
match event {
PlatformEvent::PointerDown { x, y, button: MouseButton::Left } => {
let layout = self.hud_layout();
if let Some(id) = hit_slider(&layout, *x, *y) {
self.hud_dragging_slider = Some(id);
if let Some(slider) = layout.sliders.iter().find(|s| s.id == id) {
self.apply_hud_slider(id, *x, slider.track);
}
return Some(true);
}
if let Some(control) = hit_button(&layout, *x, *y) {
self.hud_pressed_button = Some(control);
return Some(true);
}
if layout.panel.contains(*x, *y) {
return Some(true);
}
None
}
PlatformEvent::PointerMoved { x, .. } => {
if let Some(id) = self.hud_dragging_slider {
let layout = self.hud_layout();
if let Some(slider) = layout.sliders.iter().find(|s| s.id == id) {
self.apply_hud_slider(id, *x, slider.track);
}
return Some(true);
}
if self.hud_pressed_button.is_some() {
return Some(true);
}
None
}
PlatformEvent::PointerUp { x, y, button: MouseButton::Left } => {
if self.hud_dragging_slider.take().is_some() {
return Some(true);
}
if let Some(pressed) = self.hud_pressed_button.take() {
let layout = self.hud_layout();
if hit_button(&layout, *x, *y) == Some(pressed) {
self.apply_hud_control(pressed);
}
return Some(true);
}
None
}
_ => None,
}
}
fn apply_hud_slider(&mut self, id: HudSliderId, x: f64, track: Rect) {
let t = (((x - track.x) / track.width) as f32).clamp(0.0, 1.0);
let mut fly = Self::lock_fly(&self.fly);
match id {
HudSliderId::Keyboard => {
fly.set_keyboard_sensitivity(KEYBOARD_SENSITIVITY_MIN + t * (KEYBOARD_SENSITIVITY_MAX - KEYBOARD_SENSITIVITY_MIN));
}
HudSliderId::Mouse => {
fly.set_mouse_sensitivity(MOUSE_SENSITIVITY_MIN + t * (MOUSE_SENSITIVITY_MAX - MOUSE_SENSITIVITY_MIN));
}
}
}
fn apply_hud_control(&mut self, control: HudControl) {
match control {
HudControl::Fixture(fixture) => {
self.fixture.set(fixture);
rebuild_engines(&self.engine, &self.engine3d, fixture, &self.camera_fit);
}
HudControl::SetLayout(kind) => {
if !available_layout_kinds(self.fixture.get()).contains(&kind) {
return;
}
match self.dim.get() {
Dimension::TwoD => {
let mut engine = Self::lock(&self.engine);
if engine.layout.kind() == kind {
if kind == LayoutKind::Force {
let next = !engine.layout.paused();
engine.layout.set_paused(next);
} else {
engine.layout.reheat(1.0);
drop(engine);
self.camera_fit.request();
}
} else {
engine.layout.set_kind(kind);
drop(engine);
self.camera_fit.request();
}
}
Dimension::ThreeD => {
let mut engine3d = Self::lock3d(&self.engine3d);
if engine3d.layout.kind() == kind {
if kind == LayoutKind::Force {
let next = !engine3d.layout.paused();
engine3d.layout.set_paused(next);
} else {
engine3d.layout.reheat(1.0);
drop(engine3d);
self.fit_view_3d();
}
} else {
engine3d.layout.set_kind(kind);
drop(engine3d);
self.fit_view_3d();
}
}
}
}
HudControl::ToggleDimension => self.toggle_dimension(),
HudControl::ToggleNavMode => {
if self.dim.get() == Dimension::ThreeD {
self.toggle_nav_mode();
}
}
HudControl::FitView => self.fit_view_current_dimension(),
HudControl::ToggleGrid => {
if self.dim.get() == Dimension::ThreeD {
self.toggle_grid();
}
}
}
}
}
impl BlackboxAgentSurface for DemoBlackbox {
fn agent_slot_id(&self) -> &str {
BLACKBOX_SLOT
}
fn agent_kind(&self) -> &str {
"graph"
}
fn list_agent_widgets(&self) -> Vec<AgentWidget> {
match self.dim.get() {
Dimension::TwoD => DemoApp::lock(&self.engine).list_agent_widgets(),
Dimension::ThreeD => Vec::new(),
}
}
fn agent_state(&self) -> Value {
let mut state = match self.dim.get() {
Dimension::TwoD => DemoApp::lock(&self.engine).agent_state(),
Dimension::ThreeD => {
let engine3d = DemoApp::lock3d(&self.engine3d);
json!({
"node_count": engine3d.graph.node_count(),
"edge_count": engine3d.graph.edge_count(),
"hovered": engine3d.hovered().map(NodeIndex::index),
"hover": engine3d.hovered().and_then(|id| node_facts_json_3d(&engine3d, id)),
"selected": engine3d.selected().map(NodeIndex::index),
"selected_facts": engine3d.selected().and_then(|id| node_facts_json_3d(&engine3d, id)),
"selection": selection_json_3d(&engine3d),
"local_root": engine3d.local_root().map(|(node, depth)| json!({ "index": node.index(), "depth": depth })),
"filter": engine3d.filter().map(filter_json_3d),
"layout": layout_kind_str(engine3d.layout.kind()),
"layout_paused": engine3d.layout.paused(),
})
}
};
if let Value::Object(ref mut map) = state {
map.insert("dimension".to_owned(), json!(self.dim.get().code()));
map.insert("fixture".to_owned(), json!(self.fixture.get().as_str()));
map.insert("nav_mode".to_owned(), json!(self.nav_mode.get().as_str()));
map.insert("mouse_look".to_owned(), json!(self.mouse_look.get()));
let fly_velocity = DemoApp::lock_fly(&self.fly).velocity();
map.insert("fly_velocity".to_owned(), json!([fly_velocity[0], fly_velocity[1]]));
map.insert("grid".to_owned(), json!(DemoApp::lock3d(&self.engine3d).grid_enabled()));
{
let engine3d = DemoApp::lock3d(&self.engine3d);
map.insert(
"dimension_transition".to_owned(),
json!({
"active": engine3d.transition_active(),
"direction": engine3d.transition_direction().map(TransitionDirection::as_str),
"progress": engine3d.transition_progress(),
}),
);
}
map.insert("frame_profile_ema_ms".to_owned(), DemoApp::lock_profiler(&self.frame_profiler).to_json());
map.insert("hud".to_owned(), json!({ "visible": self.hud_visible.get() }));
}
state
}
fn apply_agent_action(&mut self, action: AgentAction) -> AgentActionReply {
if action.name == "set_dimension" {
let animate = action.args.get("animate").and_then(Value::as_bool).unwrap_or(true);
return match action.args.get("dim").and_then(Value::as_u64) {
Some(2) => self.set_dimension_2d(animate),
Some(3) => self.set_dimension_3d(animate),
_ => AgentActionReply::err("set_dimension requires args.dim to be 2 or 3"),
};
}
if action.name == "set_nav_mode" {
let Some(mode) = action.args.get("mode").and_then(Value::as_str).and_then(NavMode::from_str) else {
return AgentActionReply::err("set_nav_mode requires args.mode to be one of: orbit, fly");
};
self.nav_mode.set(mode);
self.mouse_look.set(true);
DemoApp::lock_fly(&self.fly).stop();
return AgentActionReply::ok_with_log(json!({ "nav_mode": mode.as_str(), "mouse_look": true }));
}
if action.name == "set_mouse_look" {
let Some(on) = action.args.get("on").and_then(Value::as_bool) else {
return AgentActionReply::err("set_mouse_look requires args.on to be true or false");
};
self.mouse_look.set(on);
return AgentActionReply::ok_with_log(json!({ "mouse_look": on }));
}
if action.name == "set_fixture" {
let Some(fixture) = action.args.get("name").and_then(Value::as_str).and_then(Fixture::from_str) else {
return AgentActionReply::err("set_fixture requires args.name to be one of: clusters, tree, hierarchy, sparse");
};
self.fixture.set(fixture);
rebuild_engines(&self.engine, &self.engine3d, fixture, &self.camera_fit);
return AgentActionReply::ok_with_log(json!({ "fixture": fixture.as_str() }));
}
if action.name == "fit_view_3d" {
self.fit3d_pending.request();
return AgentActionReply::ok_with_log(json!({ "fit_view_3d": true }));
}
if action.name == "set_grid" {
let Some(on) = action.args.get("on").and_then(Value::as_bool) else {
return AgentActionReply::err("set_grid requires args.on to be true or false");
};
DemoApp::lock3d(&self.engine3d).set_grid_enabled(on);
return AgentActionReply::ok_with_log(json!({ "grid": on }));
}
if action.name == "set_hud" {
let Some(on) = action.args.get("visible").and_then(Value::as_bool) else {
return AgentActionReply::err("set_hud requires args.visible to be true or false");
};
self.hud_visible.set(on);
return AgentActionReply::ok_with_log(json!({ "hud": { "visible": on } }));
}
if action.name == "set_layout" {
if let Some(mode) = action.args.get("mode").and_then(Value::as_str) {
let kind = match mode {
"force" => Some(LayoutKind::Force),
"hierarchical" => Some(LayoutKind::Hierarchical),
"radial" => Some(LayoutKind::Radial),
_ => None, };
let fixture = self.fixture.get();
if let Some(kind) = kind {
if !available_layout_kinds(fixture).contains(&kind) {
return AgentActionReply::err(format!(
"layout mode {mode:?} does not exist for fixture {:?} (available: {:?})",
fixture.as_str(),
available_layout_kinds(fixture).iter().map(|k| layout_kind_str(*k)).collect::<Vec<_>>(),
));
}
}
}
}
match self.dim.get() {
Dimension::TwoD => DemoApp::lock(&self.engine).apply_agent_action(action),
Dimension::ThreeD => self.apply_3d_agent_action(action),
}
}
}
impl DemoBlackbox {
fn set_dimension_3d(&mut self, animate: bool) -> AgentActionReply {
apply_dimension_3d_transition(&self.engine, &self.engine3d, &self.dim, &self.flatten_pending, animate);
if !animate {
self.fit3d_pending.request();
}
AgentActionReply::ok_with_log(json!({ "dimension": 3, "animate": animate }))
}
fn set_dimension_2d(&mut self, animate: bool) -> AgentActionReply {
apply_dimension_2d_transition(&self.engine, &self.engine3d, &self.dim, &self.flatten_pending, animate);
AgentActionReply::ok_with_log(json!({ "dimension": 2, "animate": animate }))
}
fn apply_3d_agent_action(&mut self, action: AgentAction) -> AgentActionReply {
let mut engine3d = DemoApp::lock3d(&self.engine3d);
match action.name.as_str() {
"hover_node" => {
let index_arg = action.args.get("index");
let explicit_clear =
matches!(index_arg, Some(Value::Null)) || (index_arg.is_none() && action.args.get("label").is_none());
if explicit_clear {
engine3d.hovered = None;
return AgentActionReply::ok_with_log(json!({ "hover": Value::Null }));
}
let Some(node) = resolve_node_3d(&engine3d, &action) else {
return AgentActionReply::err("hover_node requires args.index (u32), args.label (string), or {} / null to clear");
};
engine3d.hovered = Some(node);
AgentActionReply::ok_with_log(json!({ "hover": { "index": node.index() } }))
}
"select_node" => {
let Some(node) = resolve_node_3d(&engine3d, &action) else {
return AgentActionReply::err("select_node requires args.index (u32) or args.label (string)");
};
engine3d.select(node);
AgentActionReply::ok_with_log(json!({ "selected": node.index() }))
}
"clear_selection" => {
engine3d.clear_selection();
AgentActionReply::ok_with_log(json!({ "selected": Value::Null }))
}
"select_nodes" => {
let Some(indices) = action.args.get("indices").and_then(Value::as_array) else {
return AgentActionReply::err("select_nodes requires args.indices (array of u32 node indices)");
};
let mode = match parse_select_mode_3d(&action) {
Ok(m) => m,
Err(e) => return AgentActionReply::err(e),
};
let mut nodes = Vec::with_capacity(indices.len());
for v in indices {
let Some(idx) = v.as_u64() else {
return AgentActionReply::err("select_nodes args.indices entries must all be u32");
};
let node = NodeIndex(idx as u32);
if node.index() >= engine3d.graph.node_count() {
return AgentActionReply::err(format!("select_nodes index {idx} out of range"));
}
nodes.push(node);
}
engine3d.apply_selection(nodes, mode);
AgentActionReply::ok_with_log(json!({ "selection": selection_json_3d(&engine3d) }))
}
"box_select" => {
let coord = |k: &str| action.args.get(k).and_then(Value::as_f64);
let (Some(x0), Some(y0), Some(x1), Some(y1)) = (coord("x0"), coord("y0"), coord("x1"), coord("y1")) else {
return AgentActionReply::err("box_select requires args.x0/y0/x1/y1 (f64 screen coords)");
};
let mode = match parse_select_mode_3d(&action) {
Ok(m) => m,
Err(e) => return AgentActionReply::err(e),
};
let viewport = viewport_from_surface_size(self.surface_size.get());
engine3d.box_select((x0, y0), (x1, y1), mode, viewport);
AgentActionReply::ok_with_log(json!({ "selection": selection_json_3d(&engine3d) }))
}
"collapse_selection" => match engine3d.collapse_selection() {
Some(id) => AgentActionReply::ok_with_log(json!({ "collapsed": id.0 })),
None => AgentActionReply::err("collapse_selection requires a non-empty selection"),
},
"pin_selection" => {
engine3d.pin_selection();
AgentActionReply::ok_with_log(json!({ "selection": selection_json_3d(&engine3d) }))
}
"unpin_selection" => {
engine3d.unpin_selection();
AgentActionReply::ok_with_log(json!({ "selection": selection_json_3d(&engine3d) }))
}
"collapse" => {
let Some(id) = resolve_cluster_3d(&action) else {
return AgentActionReply::err("collapse requires args.cluster (u32 GroupId)");
};
if engine3d.collapse_cluster(id) {
AgentActionReply::ok_with_log(json!({ "collapsed": id.0 }))
} else {
AgentActionReply::err(format!("cluster {} not found or already collapsed", id.0))
}
}
"expand" => {
let Some(id) = resolve_cluster_3d(&action) else {
return AgentActionReply::err("expand requires args.cluster (u32 GroupId)");
};
if engine3d.expand_cluster(id) {
AgentActionReply::ok_with_log(json!({ "expanded": id.0 }))
} else {
AgentActionReply::err(format!("cluster {} not found or not collapsed", id.0))
}
}
"set_local_root" => {
let index_arg = action.args.get("index");
let explicit_clear =
matches!(index_arg, Some(Value::Null)) || (index_arg.is_none() && action.args.get("label").is_none());
if explicit_clear {
engine3d.set_local_root(None, None);
self.fit3d_pending.request();
return AgentActionReply::ok_with_log(json!({ "local_root": Value::Null }));
}
let Some(node) = resolve_node_3d(&engine3d, &action) else {
return AgentActionReply::err(
"set_local_root requires args.index (u32), args.label (string), or {} / null to clear",
);
};
let depth = action.args.get("depth").and_then(Value::as_u64).map(|d| d as u8);
engine3d.set_local_root(Some(node), depth);
self.fit3d_pending.request();
AgentActionReply::ok_with_log(json!({
"local_root": engine3d.local_root().map(|(n, d)| json!({ "index": n.index(), "depth": d })),
}))
}
"set_filter" => {
let has_clause = ["label_substring", "categories", "min_degree"].iter().any(|k| action.args.get(*k).is_some());
if !has_clause {
engine3d.set_filter(None);
return AgentActionReply::ok_with_log(json!({ "filter": Value::Null }));
}
let label_substring = action.args.get("label_substring").and_then(Value::as_str).map(str::to_owned);
let categories = action
.args
.get("categories")
.and_then(Value::as_array)
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(str::to_owned)).collect::<Vec<String>>());
let min_degree = action.args.get("min_degree").and_then(Value::as_u64).map(|v| v as u32);
let spec = FilterSpec { label_substring, categories, min_degree };
engine3d.set_filter(Some(spec));
AgentActionReply::ok_with_log(json!({ "filter": engine3d.filter().map(filter_json_3d) }))
}
"set_layout" => {
let mode_arg = action.args.get("mode").and_then(Value::as_str);
let paused_arg = action.args.get("paused").and_then(Value::as_bool);
if mode_arg.is_none() && paused_arg.is_none() {
return AgentActionReply::err(
"set_layout requires args.mode (\"force\"|\"hierarchical\"|\"radial\") and/or args.paused (bool)",
);
}
let kind = match mode_arg {
Some("force") => Some(LayoutKind::Force),
Some("hierarchical") => Some(LayoutKind::Hierarchical),
Some("radial") => Some(LayoutKind::Radial),
Some(other) => return AgentActionReply::err(format!("unknown layout mode {other:?}")),
None => None,
};
if let Some(kind) = kind {
if engine3d.layout.kind() != kind {
engine3d.layout.set_kind(kind);
self.fit3d_pending.request();
} else {
engine3d.layout.set_kind(kind);
}
}
if let Some(paused) = paused_arg {
engine3d.layout.set_paused(paused);
}
AgentActionReply::ok_with_log(json!({
"layout": layout_kind_str(engine3d.layout.kind()),
"layout_paused": engine3d.layout.paused(),
}))
}
_ => AgentActionReply::err(
"3D dimension supports hover_node/select_node/clear_selection/select_nodes/box_select/collapse_selection/pin_selection/unpin_selection/collapse/expand/set_local_root/set_filter/set_layout besides set_dimension",
),
}
}
}
impl App<NoPanel> for DemoApp {
fn init(&mut self, key: &WindowKey, layout: &mut LayoutManager<NoPanel>) {
layout.chrome_mut_for(key).visible = false;
let blackbox = DemoBlackbox {
engine: self.engine.clone(),
engine3d: self.engine3d.clone(),
dim: self.dim.clone(),
fixture: self.fixture.clone(),
camera_fit: self.camera_fit.clone(),
fit3d_pending: self.fit3d_pending.clone(),
nav_mode: self.nav_mode.clone(),
mouse_look: self.mouse_look.clone(),
fly: self.fly.clone(),
frame_profiler: self.frame_profiler.clone(),
surface_size: self.last_3d_surface_px.clone(),
flatten_pending: self.flatten_pending.clone(),
hud_visible: self.hud_visible.clone(),
};
layout.register_blackbox_agent(BLACKBOX_SLOT, Arc::new(Mutex::new(blackbox)));
}
fn ui(&mut self, win: &mut WindowCtx<'_, NoPanel>) {
let hud_visible = self.hud_visible.get();
win.layout.edges_mut().clear();
win.layout.edges_mut().add(EdgeSlot {
id: SIDEBAR_SLOT.to_owned(),
side: EdgeSide::Right,
thickness: SIDEBAR_WIDTH + 2.0 * HUD_MARGIN as f32,
visible: hud_visible,
order: 0,
..Default::default()
});
let win_rect = win.layout.last_window().unwrap_or_else(|| Rect::new(0.0, 0.0, 0.0, 0.0));
if win_rect.width > 0.0 && win_rect.height > 0.0 {
win.layout.solve(win_rect);
}
let canvas_rect = win.layout.last_solved().map(|s| s.dock_area).unwrap_or_else(|| Rect::new(0.0, 0.0, 0.0, 0.0));
win.render.set_fill_color("#0d0f14");
win.render.fill_rect(canvas_rect.x, canvas_rect.y, canvas_rect.width, canvas_rect.height);
let (hot, alpha, node_count, visible_count, selection_lines) = {
let mut engine = Self::lock(&self.engine);
engine.set_canvas_rect(canvas_rect);
engine.tick_real_time();
if self.camera_fit.needs_fit() && canvas_rect.width > 0.0 {
engine.fit_view();
self.camera_fit.clear();
}
if canvas_rect.width > 0.0 && canvas_rect.height > 0.0 {
win.render.save();
win.render.clip_rect(canvas_rect.x, canvas_rect.y, canvas_rect.width, canvas_rect.height);
engine.draw(win.render);
win.render.restore();
}
let lines = selection_lines_2d(&engine);
let snapshot = (engine.is_hot(), engine.last_tick().alpha, engine.graph.node_count(), engine.visible_nodes().len(), lines);
engine.clear_dirty();
snapshot
};
if hud_visible && win_rect.width > 0.0 && win_rect.height > 0.0 {
let hud_snapshot = self.hud_snapshot();
let card_h = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, &hud_snapshot).panel.height;
let card_origin = Rect::new(
win_rect.x + win_rect.width - HUD_MARGIN - SIDEBAR_WIDTH as f64,
win_rect.y + hud_card_y(win_rect.height, card_h),
SIDEBAR_WIDTH as f64,
card_h,
);
self.last_sidebar_body = card_origin;
let hud_layout = build_hud_layout(card_origin.x, card_origin.y, card_origin.width, &hud_snapshot);
draw_hud_static(win.render, &hud_layout, 1.0);
let status_lines =
[format!("nodes {node_count} visible {visible_count}"), format!("alpha {alpha:.4} hot {hot}")];
draw_hud_status(win.render, hud_layout.panel.x, hud_layout.status_y, &status_lines, 1.0);
draw_hud_status(win.render, hud_layout.panel.x, hud_layout.selection_y, &selection_lines, 1.0);
}
}
fn regions(&mut self) -> Vec<RenderRegion> {
let engine = Self::lock(&self.engine);
vec![engine.render_region("force-graph-demo:main")]
}
fn on_event(&mut self, event: &PlatformEvent) -> bool {
if let PlatformEvent::ScaleFactorChanged { scale } = event {
self.scale_factor = *scale;
}
if self.hud_visible.get() {
if let Some(consumed) = self.on_event_hud(event) {
return consumed;
}
}
if let PlatformEvent::KeyDown { key: KeyCode::H, .. } = event {
self.hud_visible.toggle();
return true;
}
if let PlatformEvent::KeyDown { key: KeyCode::Tab, .. } = event {
self.toggle_dimension();
return true;
}
if let PlatformEvent::KeyDown { key: KeyCode::Home | KeyCode::F, .. } = event {
self.fit_view_current_dimension();
return true;
}
if let PlatformEvent::KeyDown { key: KeyCode::Escape, .. } = event {
if self.dim.get() == Dimension::ThreeD && self.nav_mode.get() == NavMode::Fly && self.mouse_look.get() {
self.mouse_look.set(false);
return true;
}
}
if self.dim.get() == Dimension::ThreeD {
if let PlatformEvent::KeyDown { key: KeyCode::V, .. } = event {
self.toggle_nav_mode();
return true;
}
if let PlatformEvent::KeyDown { key: KeyCode::G, .. } = event {
self.toggle_grid();
return true;
}
}
match self.dim.get() {
Dimension::TwoD => {
let mut engine = Self::lock(&self.engine);
engine.on_event(event)
}
Dimension::ThreeD => match self.nav_mode.get() {
NavMode::Orbit => {
let mut engine3d = Self::lock3d(&self.engine3d);
engine3d.on_event(event, self.dim3d_viewport())
}
NavMode::Fly => self.on_event_fly(event),
},
}
}
fn cursor_capture_mode(&self) -> CursorCaptureMode {
if self.dim.get() == Dimension::ThreeD
&& self.nav_mode.get() == NavMode::Fly
&& self.mouse_look.get()
{
CursorCaptureMode::LockedHidden
} else {
CursorCaptureMode::Free
}
}
}
impl Scene3DApp<NoPanel> for DemoApp {
fn scene3d(&mut self, surf_w: u32, surf_h: u32) -> Option<Scene3DFrame> {
if self.dim.get() != Dimension::ThreeD {
self.last_3d_frame_at = None;
return None;
}
self.last_3d_surface_px.set(surf_w, surf_h);
let now = std::time::Instant::now();
let dt = match self.last_3d_frame_at {
Some(prev) => now.duration_since(prev).as_secs_f32().min(0.1),
None => 1.0 / 60.0,
};
if self.last_3d_frame_at.is_some() {
Self::lock_profiler(&self.frame_profiler).record_ms("frame_total", dt as f64 * 1000.0);
}
self.last_3d_frame_at = Some(now);
let mut engine3d = Self::lock3d(&self.engine3d);
let tick_started = std::time::Instant::now();
engine3d.tick(dt);
let tick_ms = tick_started.elapsed().as_secs_f64() * 1000.0;
if self.nav_mode.get() == NavMode::Fly {
Self::lock_fly(&self.fly).tick(dt, &mut engine3d.camera);
}
if self.fit3d_pending.needs_fit() && !engine3d.transition_active() {
engine3d.fit_view(surf_w as f32 / (surf_h.max(1) as f32));
self.fit3d_pending.clear();
}
if self.flatten_pending.is_pending() && !engine3d.transition_active() {
drop(engine3d);
flatten_3d_into_2d(&self.engine, &self.engine3d, &self.dim);
self.flatten_pending.clear();
self.last_3d_frame_at = None;
return None;
}
let scene_build_started = std::time::Instant::now();
let scene = engine3d.build_scene(surf_h as f64);
let scene_build_ms = scene_build_started.elapsed().as_secs_f64() * 1000.0;
let aspect = surf_w as f32 / (surf_h.max(1) as f32);
let camera = engine3d.camera(aspect);
let node_count = engine3d.graph.node_count();
drop(engine3d);
let (frame_total_ms, overlay_ms) = {
let mut profiler = Self::lock_profiler(&self.frame_profiler);
profiler.record_ms("tick", tick_ms);
profiler.record_ms("scene_build", scene_build_ms);
profiler.end_frame();
(profiler.stage_ms("frame_total"), profiler.stage_ms("overlay_paint"))
};
let fps = if frame_total_ms > 0.0 { 1000.0 / frame_total_ms } else { 0.0 };
let hud_snapshot = self.hud_snapshot();
let hud_visible = self.hud_visible.get();
let hud_scale = self.scale_factor;
let (hud_origin_x, hud_origin_y, hud_width) = self.hud_origin(&hud_snapshot);
let engine3d_for_overlay = self.engine3d.clone();
let profiler_for_overlay = self.frame_profiler.clone();
let overlay_viewport = Rect::new(0.0, 0.0, surf_w as f64, surf_h as f64);
let crosshair_armed = self.nav_mode.get() == NavMode::Fly && self.mouse_look.get();
let overlay: Box<dyn FnMut(&mut dyn RenderContext)> = Box::new(move |ctx: &mut dyn RenderContext| {
let overlay_started = std::time::Instant::now();
let engine3d = Self::lock3d(&engine3d_for_overlay);
engine3d.draw_overlay(ctx, &camera, overlay_viewport);
let selection_lines = selection_lines_3d(&engine3d);
if crosshair_armed {
draw_fly_crosshair(ctx, overlay_viewport);
}
drop(engine3d);
if hud_visible {
let layout = build_hud_layout(hud_origin_x, hud_origin_y, hud_width, &hud_snapshot);
draw_hud_static(ctx, &layout, hud_scale);
let lines = [
format!("nodes {node_count} visible {node_count}"),
format!("fps {fps:.0} frame {frame_total_ms:.1}ms overlay {overlay_ms:.1}ms"),
];
draw_hud_status(ctx, layout.panel.x, layout.status_y, &lines, hud_scale);
draw_hud_status(ctx, layout.panel.x, layout.selection_y, &selection_lines, hud_scale);
}
let overlay_paint_ms = overlay_started.elapsed().as_secs_f64() * 1000.0;
Self::lock_profiler(&profiler_for_overlay).record_ms("overlay_paint", overlay_paint_ms);
});
Some(Scene3DFrame { scene, camera, cached_overlay: None, overlay: Some(overlay) })
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
AppBuilder::new(DemoApp::new())
.agent_api(AGENT_PORT)
.window(
WindowSpec::new(WindowKey::new("main"), "uzor-graph — force graph demo")
.size(1400, 900)
.min_size(900, 600)
.decorations(true)
.background(0xFF_0d_0f_14)
.corner_style(CornerStyle::Rounded)
.border_color(0x00_4d_90_fe),
)
.icon_from_png(include_bytes!("../../assets/icon.png"))?
.run_with_3d()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clusters_fixture_matches_the_original_534_node_shape() {
let (graph, positions, cluster_members) = build_clusters_graph();
assert_eq!(graph.node_count(), NUM_CLUSTERS * (CLUSTER_SIZE + 1));
assert_eq!(positions.len(), graph.node_count());
assert_eq!(cluster_members.len(), NUM_CLUSTERS);
assert!(cluster_members.iter().all(|m| m.len() == CLUSTER_SIZE));
}
#[test]
fn tree_fixture_is_connected_acyclic_and_reaches_the_requested_depth() {
let (graph, positions, depths) = build_tree_internal(TREE_NODE_BUDGET, "t");
assert!(graph.node_count() > 100, "expected a few hundred nodes, got {}", graph.node_count());
assert!(graph.node_count() <= TREE_NODE_BUDGET);
assert_eq!(positions.len(), graph.node_count());
assert_eq!(depths.len(), graph.node_count());
assert_eq!(graph.edge_count(), graph.node_count() - 1, "the plain tree fixture must have zero cross-links (n-1 edges)");
let max_depth = depths.iter().copied().max().unwrap_or(0);
assert!(
max_depth >= TREE_MIN_BRANCH_DEPTH,
"expected the tree to reach at least {TREE_MIN_BRANCH_DEPTH} branching levels, got max depth {max_depth}"
);
assert!(max_depth <= TREE_MAX_DEPTH);
assert_eq!(depths.iter().filter(|&&d| d == 0).count(), 1);
}
#[test]
fn tree_fixture_via_build_fixture_has_no_collapsible_clusters() {
let (graph, positions, cluster_members) = build_tree_graph();
assert!(graph.node_count() > 100);
assert_eq!(positions.len(), graph.node_count());
assert!(cluster_members.is_empty(), "the tree fixture has no cluster-collapse concept");
}
#[test]
fn hierarchy_fixture_adds_roughly_5_percent_cross_links_over_the_underlying_tree() {
let (graph, positions, cluster_members) = build_hierarchy_graph();
assert_eq!(positions.len(), graph.node_count());
assert!(cluster_members.is_empty());
let n = graph.node_count();
let tree_edges = n - 1;
let cross_links = graph.edge_count() - tree_edges;
let expected = n as f32 * HIERARCHY_CROSS_LINK_RATIO;
assert!(
(cross_links as f32) > expected * 0.4 && (cross_links as f32) < expected * 1.6,
"expected roughly {expected:.0} cross-link edges (~{:.0}% of {n} nodes), got {cross_links}",
HIERARCHY_CROSS_LINK_RATIO * 100.0
);
}
#[test]
fn sparse_fixture_has_low_average_degree_and_a_wide_spatial_spread() {
let (graph, positions, cluster_members) = build_sparse_graph();
assert_eq!(graph.node_count(), SPARSE_NODE_COUNT);
assert_eq!(positions.len(), SPARSE_NODE_COUNT);
assert!(cluster_members.is_empty());
let total_degree: u32 = (0..graph.node_count()).map(|i| graph.degree(NodeIndex(i as u32))).sum();
let avg_degree = total_degree as f32 / graph.node_count() as f32;
assert!(avg_degree < 2.0, "sparse fixture should be low-degree, got avg degree {avg_degree:.2}");
let max_extent = positions
.iter()
.flat_map(|&(x, y)| [x.abs(), y.abs()])
.fold(0.0_f32, f32::max);
let mut longest_edge = 0.0_f32;
for (_, edge) in graph.edges() {
let (ax, ay) = positions[edge.from.index()];
let (bx, by) = positions[edge.to.index()];
let d = ((ax - bx).powi(2) + (ay - by).powi(2)).sqrt();
longest_edge = longest_edge.max(d);
}
assert!(
longest_edge > max_extent * 0.5,
"expected at least one long-range edge spanning >50% of the layout's own extent ({max_extent:.1}), longest was {longest_edge:.1}"
);
}
#[test]
fn every_fixture_is_deterministic_across_repeated_calls() {
for fixture in [Fixture::Clusters, Fixture::Tree, Fixture::Hierarchy, Fixture::Sparse] {
let (g1, p1, _) = build_fixture(fixture);
let (g2, p2, _) = build_fixture(fixture);
assert_eq!(g1.node_count(), g2.node_count(), "{fixture:?} node_count must be deterministic");
assert_eq!(g1.edge_count(), g2.edge_count(), "{fixture:?} edge_count must be deterministic");
assert_eq!(p1, p2, "{fixture:?} positions must be byte-identical across repeated calls");
for (id, node) in g1.nodes() {
let other = g2.nodes().nth(id.index()).expect("same node_count implies same index range").1;
assert_eq!(node.label, other.label, "{fixture:?} label must be deterministic at index {}", id.index());
assert_eq!(node.category, other.category, "{fixture:?} category must be deterministic at index {}", id.index());
}
}
}
#[test]
fn fixture_name_round_trips_through_as_str_and_from_str() {
for fixture in [Fixture::Clusters, Fixture::Tree, Fixture::Hierarchy, Fixture::Sparse] {
assert_eq!(Fixture::from_str(fixture.as_str()), Some(fixture));
assert_eq!(Fixture::from_code(fixture.code()), fixture);
}
assert_eq!(Fixture::from_str("not-a-real-fixture"), None);
}
#[test]
fn fixture_state_defaults_to_clusters_and_get_reflects_the_last_set() {
let state = FixtureState::new();
assert_eq!(state.get(), Fixture::Clusters);
state.set(Fixture::Sparse);
assert_eq!(state.get(), Fixture::Sparse);
}
#[test]
fn hit_button_resolves_the_correct_fixture_button_and_none_outside_any_zone() {
let snap = HudSnapshot {
dim: Dimension::ThreeD,
fixture: Fixture::Clusters,
nav_mode: NavMode::Orbit,
grid_enabled: false,
keyboard_sensitivity: 1.0,
mouse_sensitivity: 1.0,
layout_kind: LayoutKind::Force,
layout_paused: false,
selection_line_count: 1,
};
let layout = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, &snap);
let tree_button = layout.buttons.iter().find(|b| b.control == HudControl::Fixture(Fixture::Tree)).expect("tree button must exist");
let (cx, cy) = (tree_button.rect.center_x(), tree_button.rect.center_y());
assert_eq!(hit_button(&layout, cx, cy), Some(HudControl::Fixture(Fixture::Tree)));
assert_eq!(hit_button(&layout, layout.panel.x + 1.0, cy), None);
assert_eq!(hit_button(&layout, layout.panel.x - 50.0, layout.panel.y - 50.0), None);
}
#[test]
fn hud_layout_omits_3d_only_controls_while_2d_is_active() {
let snap_2d = HudSnapshot {
dim: Dimension::TwoD,
fixture: Fixture::Clusters,
nav_mode: NavMode::Orbit,
grid_enabled: false,
keyboard_sensitivity: 1.0,
mouse_sensitivity: 1.0,
layout_kind: LayoutKind::Force,
layout_paused: false,
selection_line_count: 1,
};
let layout_2d = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, &snap_2d);
assert!(!layout_2d.buttons.iter().any(|b| b.control == HudControl::ToggleNavMode), "Orbit/Fly button must not appear in 2D");
assert!(!layout_2d.buttons.iter().any(|b| b.control == HudControl::ToggleGrid), "Grid button must not appear in 2D");
assert!(layout_2d.sliders.is_empty(), "sensitivity sliders are a 3D fly-mode-only section");
assert!(layout_2d.buttons.iter().any(|b| b.control == HudControl::ToggleDimension), "the 2D/3D toggle must always appear");
assert!(layout_2d.buttons.iter().any(|b| b.control == HudControl::FitView), "Fit must always appear (not 3D-only)");
assert!(layout_2d.buttons.iter().any(|b| b.control == HudControl::SetLayout(LayoutKind::Force)), "LAYOUT buttons must appear in 2D");
let snap_3d_fly = HudSnapshot { dim: Dimension::ThreeD, nav_mode: NavMode::Fly, ..snap_2d };
let layout_3d_fly = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, &snap_3d_fly);
assert!(layout_3d_fly.buttons.iter().any(|b| b.control == HudControl::ToggleNavMode));
assert!(layout_3d_fly.buttons.iter().any(|b| b.control == HudControl::ToggleGrid));
assert_eq!(layout_3d_fly.sliders.len(), 2, "keyboard + mouse sensitivity sliders while 3D fly is active");
}
#[test]
fn hud_layout_shows_only_the_layout_modes_that_exist_for_the_fixture() {
let base = HudSnapshot {
dim: Dimension::TwoD,
fixture: Fixture::Clusters,
nav_mode: NavMode::Orbit,
grid_enabled: false,
keyboard_sensitivity: 1.0,
mouse_sensitivity: 1.0,
layout_kind: LayoutKind::Force,
layout_paused: false,
selection_line_count: 1,
};
for dim in [Dimension::TwoD, Dimension::ThreeD] {
for fixture in [Fixture::Clusters, Fixture::Sparse] {
let snap = HudSnapshot { dim, fixture, ..base };
let layout = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, &snap);
let modes: Vec<_> = layout.buttons.iter().filter(|b| matches!(b.control, HudControl::SetLayout(_))).collect();
assert_eq!(modes.len(), 1, "{fixture:?}/{dim:?}: mesh-shaped fixtures have exactly one layout mode");
assert!(modes[0].control == HudControl::SetLayout(LayoutKind::Force) && modes[0].active);
}
for fixture in [Fixture::Tree, Fixture::Hierarchy] {
let snap = HudSnapshot { dim, fixture, layout_kind: LayoutKind::Hierarchical, ..base };
let layout = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, &snap);
let modes: Vec<_> = layout.buttons.iter().filter(|b| matches!(b.control, HudControl::SetLayout(_))).collect();
assert_eq!(modes.len(), 3, "{fixture:?}/{dim:?}: tree-shaped fixtures have all three layout modes");
assert!(modes.iter().any(|b| b.control == HudControl::SetLayout(LayoutKind::Force) && !b.active));
assert!(modes.iter().any(|b| b.control == HudControl::SetLayout(LayoutKind::Hierarchical) && b.active));
assert!(modes.iter().any(|b| b.control == HudControl::SetLayout(LayoutKind::Radial) && !b.active));
}
}
}
#[test]
fn hud_layout_scale_invariant_holds_for_a_synthetic_click_at_an_arbitrary_scale() {
let snap = HudSnapshot {
dim: Dimension::ThreeD,
fixture: Fixture::Sparse,
nav_mode: NavMode::Orbit,
grid_enabled: true,
keyboard_sensitivity: 1.2,
mouse_sensitivity: 0.8,
layout_kind: LayoutKind::Force,
layout_paused: false,
selection_line_count: 5,
};
let layout = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, &snap);
let button = layout.buttons.first().expect("at least one button");
let logical = (button.rect.center_x(), button.rect.center_y());
assert_eq!(hit_button(&layout, logical.0, logical.1), Some(button.control));
for scale in [1.0_f64, 1.25, 1.5, 2.0] {
let physical = (logical.0 * scale, logical.1 * scale);
let drawn = scaled_rect(button.rect, scale);
assert!(
drawn.contains(physical.0, physical.1),
"at scale {scale}: physical click {physical:?} must land inside the scaled draw rect {drawn:?}"
);
}
}
#[test]
fn tab_key_toggles_dimension_from_2d_to_3d_and_back() {
let mut app = DemoApp::new();
assert_eq!(app.dim.get(), Dimension::TwoD);
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::Tab, modifiers: uzor::input::ModifierKeys::default() });
assert_eq!(app.dim.get(), Dimension::ThreeD, "Tab must switch 2D -> 3D");
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::Tab, modifiers: uzor::input::ModifierKeys::default() });
assert!(app.flatten_pending.is_pending(), "Tab from 3D must request the animated flatten-to-2D");
}
#[test]
fn v_key_toggles_nav_mode_only_while_3d_is_active_and_is_a_noop_in_2d() {
let mut app = DemoApp::new();
assert_eq!(app.nav_mode.get(), NavMode::Orbit);
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::V, modifiers: uzor::input::ModifierKeys::default() });
assert_eq!(app.nav_mode.get(), NavMode::Orbit, "V must be a no-op while 2D is active");
app.dim.set(Dimension::ThreeD);
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::V, modifiers: uzor::input::ModifierKeys::default() });
assert_eq!(app.nav_mode.get(), NavMode::Fly, "V must toggle orbit -> fly while 3D is active");
}
#[test]
fn h_key_toggles_hud_visibility_and_defaults_to_shown() {
let mut app = DemoApp::new();
assert!(app.hud_visible.get());
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::H, modifiers: uzor::input::ModifierKeys::default() });
assert!(!app.hud_visible.get());
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::H, modifiers: uzor::input::ModifierKeys::default() });
assert!(app.hud_visible.get());
}
#[test]
fn escape_key_exits_fly_mouse_look_only_when_active_in_3d_fly() {
let mut app = DemoApp::new();
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::Escape, modifiers: uzor::input::ModifierKeys::default() });
assert!(app.mouse_look.get(), "mouse_look defaults true and Escape must not touch it outside 3D fly");
app.dim.set(Dimension::ThreeD);
app.nav_mode.set(NavMode::Fly);
assert!(app.mouse_look.get());
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::Escape, modifiers: uzor::input::ModifierKeys::default() });
assert!(!app.mouse_look.get(), "Escape must release fly mouse-look");
app.on_event(&PlatformEvent::KeyDown { key: KeyCode::Escape, modifiers: uzor::input::ModifierKeys::default() });
assert!(!app.mouse_look.get());
}
#[test]
fn pointer_down_inside_the_hud_panel_is_consumed_by_on_event() {
let mut app = DemoApp::new();
app.dim.set(Dimension::ThreeD);
let panel = app.hud_layout().panel;
let consumed =
app.on_event(&PlatformEvent::PointerDown { x: panel.x + 20.0, y: panel.y + 5.0, button: MouseButton::Left });
assert!(consumed, "a PointerDown inside the HUD panel must be consumed, never fall through to the graph engine");
assert_eq!(app.dim.get(), Dimension::ThreeD);
assert_eq!(app.nav_mode.get(), NavMode::Orbit);
}
#[test]
fn fixture_button_click_rebuilds_both_engines_from_the_clicked_fixture() {
let mut app = DemoApp::new();
app.dim.set(Dimension::ThreeD);
let layout = app.hud_layout();
let tree_button = layout.buttons.iter().find(|b| b.control == HudControl::Fixture(Fixture::Tree)).expect("tree button must exist");
let (x, y) = (tree_button.rect.center_x(), tree_button.rect.center_y());
assert!(app.on_event(&PlatformEvent::PointerDown { x, y, button: MouseButton::Left }));
assert!(app.on_event(&PlatformEvent::PointerUp { x, y, button: MouseButton::Left }));
assert_eq!(app.fixture.get(), Fixture::Tree);
let node_count = DemoApp::lock3d(&app.engine3d).graph.node_count();
assert!(node_count > 100, "the tree fixture has a few hundred nodes, got {node_count}");
}
#[test]
fn hud_panel_rect_consistency_both_dimensions_dock_flush_right_with_the_same_width() {
let mut app = DemoApp::new();
let snap = app.hud_snapshot();
app.last_sidebar_body = Rect::new(1080.0, 40.0, SIDEBAR_WIDTH as f64, 860.0);
assert_eq!(app.hud_origin(&snap), (1080.0, 40.0, SIDEBAR_WIDTH as f64));
app.dim.set(Dimension::ThreeD);
app.last_3d_surface_px.set(1400, 900);
let snap3 = app.hud_snapshot();
let card_h = build_hud_layout(0.0, 0.0, SIDEBAR_WIDTH as f64, &snap3).panel.height;
let (x3, y3, w3) = app.hud_origin(&snap3);
assert_eq!(w3, SIDEBAR_WIDTH as f64, "3D card width must match the 2D card's own width");
assert_eq!(x3, 1400.0 - HUD_MARGIN - SIDEBAR_WIDTH as f64, "3D card docks toward the right edge with HUD_MARGIN air");
assert_eq!(y3, (900.0 - card_h) / 2.0, "the card is vertically centered — equal air above and below");
assert!((y3 + card_h / 2.0 - 450.0).abs() < 1e-9, "card center must sit at the viewport's vertical center");
assert!(x3 > 0.0, "must not sit at a left-anchored origin — the exact defect the owner reported");
app.last_3d_surface_px.set(1920, 1080);
let (x3_wide, y3_tall, w3_wide) = app.hud_origin(&snap3);
assert_eq!(w3_wide, SIDEBAR_WIDTH as f64);
assert_eq!(x3_wide, 1920.0 - HUD_MARGIN - SIDEBAR_WIDTH as f64);
assert!(x3_wide > x3, "a wider window must dock the panel further right, not leave it in place");
assert!(y3_tall > y3, "a taller window must re-center the card lower, not leave it pinned at a fixed top offset");
}
#[test]
fn default_layout_kind_matches_the_owner_specified_per_fixture_mapping() {
assert_eq!(default_layout_kind_for_fixture(Fixture::Clusters), LayoutKind::Force);
assert_eq!(default_layout_kind_for_fixture(Fixture::Sparse), LayoutKind::Force);
assert_eq!(default_layout_kind_for_fixture(Fixture::Tree), LayoutKind::Hierarchical);
assert_eq!(default_layout_kind_for_fixture(Fixture::Hierarchy), LayoutKind::Hierarchical);
}
#[test]
fn rebuild_engines_applies_the_default_layout_kind_to_both_engines_per_fixture() {
let engine = Arc::new(Mutex::new(Engine::new(DemoGraph::new(), GraphLayoutMode::default())));
let engine3d = Arc::new(Mutex::new(Engine3D::new(DemoGraph::new(), GraphLayoutMode3D::default())));
let camera_fit = CameraFitFlag::new();
for (fixture, expected) in [
(Fixture::Clusters, LayoutKind::Force),
(Fixture::Tree, LayoutKind::Hierarchical),
(Fixture::Hierarchy, LayoutKind::Hierarchical),
(Fixture::Sparse, LayoutKind::Force),
] {
rebuild_engines(&engine, &engine3d, fixture, &camera_fit);
assert_eq!(DemoApp::lock(&engine).layout.kind(), expected, "2D {fixture:?} must default to {expected:?}");
assert_eq!(DemoApp::lock3d(&engine3d).layout.kind(), expected, "3D {fixture:?} must default to {expected:?}");
}
}
#[test]
fn hud_layout_reflects_per_fixture_layout_availability_in_both_dimensions() {
let app = DemoApp::new();
let layout = app.hud_layout();
let modes: Vec<_> = layout.buttons.iter().filter(|b| matches!(b.control, HudControl::SetLayout(_))).collect();
assert_eq!(modes.len(), 1, "clusters must expose exactly one layout mode");
assert!(modes[0].control == HudControl::SetLayout(LayoutKind::Force) && modes[0].active);
app.fixture.set(Fixture::Tree);
rebuild_engines(&app.engine, &app.engine3d, Fixture::Tree, &app.camera_fit);
let layout = app.hud_layout();
let layered_button = layout.buttons.iter().find(|b| b.control == HudControl::SetLayout(LayoutKind::Hierarchical));
assert!(layered_button.is_some_and(|b| b.active), "LAYERED must exist and be the tree fixture's active default");
assert!(layout.buttons.iter().any(|b| b.control == HudControl::SetLayout(LayoutKind::Radial)), "RADIAL button must exist too");
app.dim.set(Dimension::ThreeD);
let layout_3d = app.hud_layout();
let layout_buttons_3d: Vec<_> =
layout_3d.buttons.iter().filter(|b| matches!(b.control, HudControl::SetLayout(_))).collect();
assert_eq!(layout_buttons_3d.len(), 3, "3D must show the same 3 tree-fixture modes as 2D");
assert!(layout_buttons_3d.iter().any(|b| b.control == HudControl::SetLayout(LayoutKind::Force) && !b.active));
assert!(layout_buttons_3d.iter().any(|b| b.control == HudControl::SetLayout(LayoutKind::Hierarchical) && b.active));
assert!(layout_buttons_3d.iter().any(|b| b.control == HudControl::SetLayout(LayoutKind::Radial) && !b.active));
}
#[test]
fn layout_button_click_switches_the_2d_engines_layout_kind_and_requests_a_camera_fit() {
let mut app = DemoApp::new();
app.fixture.set(Fixture::Tree);
rebuild_engines(&app.engine, &app.engine3d, Fixture::Tree, &app.camera_fit);
app.camera_fit.clear();
let layout = app.hud_layout();
let radial_button = layout
.buttons
.iter()
.find(|b| b.control == HudControl::SetLayout(LayoutKind::Radial))
.expect("RADIAL button must exist for the tree fixture while 2D is active");
let (x, y) = (radial_button.rect.center_x(), radial_button.rect.center_y());
assert!(app.on_event(&PlatformEvent::PointerDown { x, y, button: MouseButton::Left }));
assert!(app.on_event(&PlatformEvent::PointerUp { x, y, button: MouseButton::Left }));
assert_eq!(DemoApp::lock(&app.engine).layout.kind(), LayoutKind::Radial);
assert!(app.camera_fit.needs_fit(), "switching layout must request a camera re-fit, same convention a fixture switch already follows");
}
#[test]
fn re_clicking_the_active_force_button_pauses_and_resumes_in_place_in_3d() {
let mut app = DemoApp::new();
app.dim.set(Dimension::ThreeD);
assert!(!DemoApp::lock3d(&app.engine3d).layout.paused());
let layout = app.hud_layout();
let force_button = layout
.buttons
.iter()
.find(|b| b.control == HudControl::SetLayout(LayoutKind::Force))
.expect("FORCE button must exist in 3D");
assert!(force_button.active, "FORCE must be the default active mode");
let (x, y) = (force_button.rect.center_x(), force_button.rect.center_y());
assert!(app.on_event(&PlatformEvent::PointerDown { x, y, button: MouseButton::Left }));
assert!(app.on_event(&PlatformEvent::PointerUp { x, y, button: MouseButton::Left }));
assert!(DemoApp::lock3d(&app.engine3d).layout.paused(), "re-clicking the active FORCE button must pause the sim");
let paused_layout = app.hud_layout();
let paused_button =
paused_layout.buttons.iter().find(|b| b.control == HudControl::SetLayout(LayoutKind::Force)).expect("still there");
assert!(paused_button.paused, "the HUD button itself must report the paused visual state");
assert!(paused_button.label.contains("PAUSED"));
assert!(app.on_event(&PlatformEvent::PointerDown { x, y, button: MouseButton::Left }));
assert!(app.on_event(&PlatformEvent::PointerUp { x, y, button: MouseButton::Left }));
assert!(!DemoApp::lock3d(&app.engine3d).layout.paused(), "a second re-click must resume the sim");
}
#[test]
fn re_clicking_the_active_force_button_pauses_and_resumes_in_place_in_2d() {
let mut app = DemoApp::new();
assert!(!DemoApp::lock(&app.engine).layout.paused());
let layout = app.hud_layout();
let force_button = layout
.buttons
.iter()
.find(|b| b.control == HudControl::SetLayout(LayoutKind::Force))
.expect("FORCE button must exist in 2D");
let (x, y) = (force_button.rect.center_x(), force_button.rect.center_y());
assert!(app.on_event(&PlatformEvent::PointerDown { x, y, button: MouseButton::Left }));
assert!(app.on_event(&PlatformEvent::PointerUp { x, y, button: MouseButton::Left }));
assert!(DemoApp::lock(&app.engine).layout.paused(), "re-clicking the active FORCE button must pause the sim");
assert!(app.on_event(&PlatformEvent::PointerDown { x, y, button: MouseButton::Left }));
assert!(app.on_event(&PlatformEvent::PointerUp { x, y, button: MouseButton::Left }));
assert!(!DemoApp::lock(&app.engine).layout.paused(), "a second re-click must resume the sim");
}
#[test]
fn re_clicking_the_active_layered_button_re_runs_the_layout_and_requests_a_fit() {
let mut app = DemoApp::new();
app.fixture.set(Fixture::Tree);
rebuild_engines(&app.engine, &app.engine3d, Fixture::Tree, &app.camera_fit);
assert_eq!(DemoApp::lock(&app.engine).layout.kind(), LayoutKind::Hierarchical);
app.camera_fit.clear();
{
let mut engine = DemoApp::lock(&app.engine);
engine.particles[0].x = 123_456.0;
}
let layout = app.hud_layout();
let layered_button = layout
.buttons
.iter()
.find(|b| b.control == HudControl::SetLayout(LayoutKind::Hierarchical))
.expect("LAYERED must be active for the tree fixture");
assert!(layered_button.active);
let (x, y) = (layered_button.rect.center_x(), layered_button.rect.center_y());
assert!(app.on_event(&PlatformEvent::PointerDown { x, y, button: MouseButton::Left }));
assert!(app.on_event(&PlatformEvent::PointerUp { x, y, button: MouseButton::Left }));
assert_eq!(DemoApp::lock(&app.engine).layout.kind(), LayoutKind::Hierarchical, "re-click must not change kind");
assert!(app.camera_fit.needs_fit(), "re-running a one-shot layout must request a camera re-fit");
DemoApp::lock(&app.engine).tick(1.0 / 60.0);
assert_ne!(DemoApp::lock(&app.engine).particles[0].x, 123_456.0);
}
#[test]
fn clicking_an_inactive_layout_button_in_3d_switches_kind_and_fits() {
let mut app = DemoApp::new();
app.fixture.set(Fixture::Tree);
rebuild_engines(&app.engine, &app.engine3d, Fixture::Tree, &app.camera_fit);
app.dim.set(Dimension::ThreeD);
assert_eq!(DemoApp::lock3d(&app.engine3d).layout.kind(), LayoutKind::Hierarchical);
let layout = app.hud_layout();
let radial_button = layout
.buttons
.iter()
.find(|b| b.control == HudControl::SetLayout(LayoutKind::Radial))
.expect("RADIAL button must exist in 3D");
let (x, y) = (radial_button.rect.center_x(), radial_button.rect.center_y());
assert!(app.on_event(&PlatformEvent::PointerDown { x, y, button: MouseButton::Left }));
assert!(app.on_event(&PlatformEvent::PointerUp { x, y, button: MouseButton::Left }));
assert_eq!(DemoApp::lock3d(&app.engine3d).layout.kind(), LayoutKind::Radial);
assert!(!DemoApp::lock3d(&app.engine3d).layout.paused(), "switching kind must not leave the new kind paused");
}
#[test]
fn hierarchical_layout_settles_the_tree_fixture_into_strict_top_down_layers() {
let (graph, positions, _clusters) = build_fixture(Fixture::Tree);
let mut engine = Engine::new(graph, GraphLayoutMode::default());
engine.seed_positions(&positions);
engine.layout.set_kind(LayoutKind::Hierarchical);
engine.tick(1.0 / 60.0);
let root = NodeIndex(0);
let root_y = engine.particles[root.index()].y;
let mut child_edges_checked = 0usize;
for (_, edge) in engine.graph.edges() {
if edge.from == root {
child_edges_checked += 1;
let child_y = engine.particles[edge.to.index()].y;
assert!(
root_y < child_y,
"hierarchical layout must place the root strictly ABOVE (a shallower layer than) its child \
(root_y={root_y}, child_y={child_y})"
);
}
}
assert!(child_edges_checked > 0, "the tree fixture's root must have at least one child edge to prove layering against");
engine.tick(1.0 / 60.0);
assert_eq!(engine.particles[root.index()].y, root_y, "hierarchical layout is one-shot — a second tick must not move the root");
}
}
#[cfg(test)]
mod screenshot_diff {
use uzor::framework::render_control::RenderControl;
use uzor::platform::types::RenderBackend;
use uzor_examples::parity_harness::{
attach_headless_window, compare_tight, dump_comparison_pngs, record_via_urx_ctx, render_via_urx_cpu,
render_via_urx_native, render_via_vello_cpu, ChannelTolerance,
};
use uzor_urx_core::scene::Scene;
use super::*;
const WIDTH: u32 = 1280;
const HEIGHT: u32 = 800;
struct MockRenderControl {
backend: RenderBackend,
fps_limit: u32,
msaa_samples: u8,
vsync: bool,
}
impl RenderControl for MockRenderControl {
fn active_backend(&self) -> RenderBackend {
self.backend
}
fn available_backends(&self) -> Vec<RenderBackend> {
vec![self.backend]
}
fn set_backend(&mut self, b: RenderBackend) {
self.backend = b;
}
fn fps_limit(&self) -> u32 {
self.fps_limit
}
fn set_fps_limit(&mut self, fps: u32) {
self.fps_limit = fps;
}
fn msaa_samples(&self) -> u8 {
self.msaa_samples
}
fn set_msaa_samples(&mut self, n: u8) {
self.msaa_samples = n;
}
fn vsync(&self) -> bool {
self.vsync
}
fn set_vsync(&mut self, on: bool) {
self.vsync = on;
}
}
fn mock_render_control() -> MockRenderControl {
MockRenderControl { backend: RenderBackend::TinySkia, fps_limit: 0, msaa_samples: 0, vsync: false }
}
fn record_2d_frame(width: u32, height: u32) -> Scene {
record_via_urx_ctx(width, height, |ctx| {
let mut app = DemoApp::new();
let mut layout = LayoutManager::<NoPanel>::default();
attach_headless_window(&mut layout, width, height);
let key = WindowKey::new("parity-fixture");
let rect = Rect::new(0.0, 0.0, width as f64, height as f64);
let mut rc = mock_render_control();
let mut win = WindowCtx { key: &key, layout: &mut layout, render: ctx, rect, render_control: &mut rc };
app.ui(&mut win);
})
}
fn record_3d_overlay(width: u32, height: u32) -> Option<Scene> {
let mut app = DemoApp::new();
app.toggle_dimension();
let mut frame = app.scene3d(width, height)?;
let mut overlay = frame.overlay.take()?;
Some(record_via_urx_ctx(width, height, |ctx| overlay(ctx)))
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn force_graph_2d_frame_native_matches_cpu_within_the_base_tier() {
let scene = record_2d_frame(WIDTH, HEIGHT);
let cpu = render_via_urx_cpu(&scene, WIDTH, HEIGHT);
let Some(native) = render_via_urx_native(&scene, WIDTH, HEIGHT) else {
eprintln!("force_graph_2d_frame_native_matches_cpu_within_the_base_tier: no GPU/software adapter available; skipping");
return;
};
let tol = ChannelTolerance::default();
let report = compare_tight(&cpu, &native, tol);
println!(
"force_graph_2d_frame urx-cpu-vs-native: {:.3}% differing (edge {}), budget {:.2}%, max_channel_diff {}",
report.differing_fraction * 100.0,
tol.edge,
tol.max_differing_fraction * 100.0,
report.max_channel_diff,
);
if !report.within_budget {
dump_comparison_pngs("force_graph_2d_urx_cpu_vs_native", WIDTH, HEIGHT, &cpu, &native);
}
assert!(
report.within_budget,
"force_graph 2D frame exceeded the base tolerance tier: {:.3}% differing (budget {:.2}%), max_channel_diff {}",
report.differing_fraction * 100.0,
tol.max_differing_fraction * 100.0,
report.max_channel_diff,
);
}
#[test]
#[ignore = "needs a headless GPU adapter; dumps PNGs for human review, not a hard gate"]
fn force_graph_2d_frame_native_vs_vello_visual_dump() {
let vello = render_via_vello_cpu(WIDTH, HEIGHT, |ctx| {
let mut app = DemoApp::new();
let mut layout = LayoutManager::<NoPanel>::default();
attach_headless_window(&mut layout, WIDTH, HEIGHT);
let key = WindowKey::new("parity-fixture");
let rect = Rect::new(0.0, 0.0, WIDTH as f64, HEIGHT as f64);
let mut rc = mock_render_control();
let mut win = WindowCtx { key: &key, layout: &mut layout, render: ctx, rect, render_control: &mut rc };
app.ui(&mut win);
});
let scene = record_2d_frame(WIDTH, HEIGHT);
let Some(native) = render_via_urx_native(&scene, WIDTH, HEIGHT) else {
eprintln!("force_graph_2d_frame_native_vs_vello_visual_dump: no GPU/software adapter available; skipping");
return;
};
dump_comparison_pngs("force_graph_2d_urx_native_vs_vello", WIDTH, HEIGHT, &native, &vello);
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn force_graph_3d_overlay_native_matches_cpu_within_the_base_tier() {
let Some(scene) = record_3d_overlay(WIDTH, HEIGHT) else {
eprintln!("force_graph_3d_overlay_native_matches_cpu_within_the_base_tier: scene3d() returned None; skipping");
return;
};
let cpu = render_via_urx_cpu(&scene, WIDTH, HEIGHT);
let Some(native) = render_via_urx_native(&scene, WIDTH, HEIGHT) else {
eprintln!("force_graph_3d_overlay_native_matches_cpu_within_the_base_tier: no GPU/software adapter available; skipping");
return;
};
let tol = ChannelTolerance::default();
let report = compare_tight(&cpu, &native, tol);
println!(
"force_graph_3d_overlay urx-cpu-vs-native: {:.3}% differing (edge {}), budget {:.2}%, max_channel_diff {}",
report.differing_fraction * 100.0,
tol.edge,
tol.max_differing_fraction * 100.0,
report.max_channel_diff,
);
if !report.within_budget {
dump_comparison_pngs("force_graph_3d_overlay_urx_cpu_vs_native", WIDTH, HEIGHT, &cpu, &native);
}
assert!(
report.within_budget,
"force_graph 3D overlay exceeded the base tolerance tier: {:.3}% differing (budget {:.2}%), max_channel_diff {}",
report.differing_fraction * 100.0,
tol.max_differing_fraction * 100.0,
report.max_channel_diff,
);
}
#[test]
#[ignore = "needs a headless GPU adapter; dumps PNGs for human review, not a hard gate"]
fn force_graph_3d_overlay_native_vs_vello_visual_dump() {
let vello = render_via_vello_cpu(WIDTH, HEIGHT, |ctx| {
let mut app = DemoApp::new();
app.toggle_dimension();
if let Some(mut frame) = app.scene3d(WIDTH, HEIGHT) {
if let Some(mut overlay) = frame.overlay.take() {
overlay(ctx);
}
}
});
let Some(scene) = record_3d_overlay(WIDTH, HEIGHT) else {
eprintln!("force_graph_3d_overlay_native_vs_vello_visual_dump: scene3d() returned None; skipping");
return;
};
let Some(native) = render_via_urx_native(&scene, WIDTH, HEIGHT) else {
eprintln!("force_graph_3d_overlay_native_vs_vello_visual_dump: no GPU/software adapter available; skipping");
return;
};
dump_comparison_pngs("force_graph_3d_overlay_urx_native_vs_vello", WIDTH, HEIGHT, &native, &vello);
}
}