use crate::graph::SimTopology;
use crate::particle::Particle;
use super::force_directed_3d::{ForceDirectedLayout3D, ForceParams3D};
use super::hierarchical_3d::{HierarchicalLayout3D, HierarchicalParams3D};
use super::mode::LayoutKind;
use super::radial_3d::{RadialLayout3D, RadialParams3D};
use super::{Layout, LayoutTickResult};
pub struct GraphLayoutMode3D {
kind: LayoutKind,
force: ForceDirectedLayout3D,
hierarchical: HierarchicalLayout3D,
radial: RadialLayout3D,
paused: bool,
}
impl Default for GraphLayoutMode3D {
fn default() -> Self {
Self {
kind: LayoutKind::Force,
force: ForceDirectedLayout3D::default(),
hierarchical: HierarchicalLayout3D::default(),
radial: RadialLayout3D::default(),
paused: false,
}
}
}
impl GraphLayoutMode3D {
pub fn kind(&self) -> LayoutKind {
self.kind
}
pub fn set_kind(&mut self, kind: LayoutKind) {
self.kind = kind;
self.paused = false;
match kind {
LayoutKind::Force => self.force.reheat(1.0),
LayoutKind::Hierarchical => self.hierarchical.reheat(1.0),
LayoutKind::Radial => self.radial.reheat(1.0),
}
}
pub fn paused(&self) -> bool {
self.paused
}
pub fn set_paused(&mut self, paused: bool) {
self.paused = paused;
}
pub fn force_params(&self) -> &ForceParams3D {
self.force.params()
}
pub fn set_force_params(&mut self, params: ForceParams3D) {
self.force.set_params(params);
}
pub fn hierarchical_params(&self) -> &HierarchicalParams3D {
self.hierarchical.params()
}
pub fn set_hierarchical_params(&mut self, params: HierarchicalParams3D) {
self.hierarchical.set_params(params);
}
pub fn radial_params(&self) -> &RadialParams3D {
self.radial.params()
}
pub fn set_radial_params(&mut self, params: RadialParams3D) {
self.radial.set_params(params);
}
}
impl Layout for GraphLayoutMode3D {
fn tick(&mut self, topo: &SimTopology<'_>, particles: &mut [Particle], dt: f32) -> LayoutTickResult {
if self.paused {
return LayoutTickResult { alpha: 0.0, max_displacement: 0.0, settled: true };
}
match self.kind {
LayoutKind::Force => self.force.tick(topo, particles, dt),
LayoutKind::Hierarchical => self.hierarchical.tick(topo, particles, dt),
LayoutKind::Radial => self.radial.tick(topo, particles, dt),
}
}
fn reheat(&mut self, alpha: f32) {
match self.kind {
LayoutKind::Force => self.force.reheat(alpha),
LayoutKind::Hierarchical => self.hierarchical.reheat(alpha),
LayoutKind::Radial => self.radial.reheat(alpha),
}
}
fn is_settled(&self) -> bool {
if self.paused {
return true;
}
match self.kind {
LayoutKind::Force => self.force.is_settled(),
LayoutKind::Hierarchical => self.hierarchical.is_settled(),
LayoutKind::Radial => self.radial.is_settled(),
}
}
fn set_alpha_target(&mut self, target: f32) {
match self.kind {
LayoutKind::Force => self.force.set_alpha_target(target),
LayoutKind::Hierarchical => self.hierarchical.set_alpha_target(target),
LayoutKind::Radial => self.radial.set_alpha_target(target),
}
}
fn alpha_target(&self) -> f32 {
match self.kind {
LayoutKind::Force => self.force.alpha_target(),
LayoutKind::Hierarchical => self.hierarchical.alpha_target(),
LayoutKind::Radial => self.radial.alpha_target(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{NodeIndex, SimEdge};
fn topo(node_count: usize, edges: &[SimEdge]) -> SimTopology<'_> {
SimTopology { node_count, edges, degree: &[], radii: vec![1.0; node_count] }
}
#[test]
fn set_kind_switches_which_algorithm_tick_dispatches_to() {
let edges = [SimEdge { from: NodeIndex(0), to: NodeIndex(1), weight: 1.0 }];
let t = topo(2, &edges);
let mut particles = vec![Particle::default(); 2];
let mut mode = GraphLayoutMode3D::default();
assert_eq!(mode.kind(), LayoutKind::Force);
mode.set_kind(LayoutKind::Hierarchical);
assert_eq!(mode.kind(), LayoutKind::Hierarchical);
let r = mode.tick(&t, &mut particles, 1.0 / 60.0);
assert!(r.settled);
assert!(particles[0].y > particles[1].y, "root must sit above its child in Y-up world");
}
#[test]
fn kind_switch_round_trips_between_force_hierarchical_and_radial() {
let mut mode = GraphLayoutMode3D::default();
for kind in [LayoutKind::Hierarchical, LayoutKind::Radial, LayoutKind::Force, LayoutKind::Radial] {
mode.set_kind(kind);
assert_eq!(mode.kind(), kind);
}
}
#[test]
fn set_kind_clears_a_previous_pause() {
let mut mode = GraphLayoutMode3D::default();
mode.set_paused(true);
assert!(mode.paused());
mode.set_kind(LayoutKind::Hierarchical);
assert!(!mode.paused(), "switching kind must clear a stale pause from the previous kind");
}
#[test]
fn paused_force_layout_does_not_move_particles_and_reports_settled() {
let edges: Vec<SimEdge> = Vec::new();
let t = topo(3, &edges);
let mut particles = vec![Particle::at3(-10.0, 0.0, 0.0), Particle::at3(10.0, 0.0, 0.0), Particle::at3(0.0, 15.0, 5.0)];
let mut mode = GraphLayoutMode3D::default();
mode.tick(&t, &mut particles, 1.0 / 60.0);
mode.tick(&t, &mut particles, 1.0 / 60.0);
let before = particles.clone();
mode.set_paused(true);
for _ in 0..10 {
let r = mode.tick(&t, &mut particles, 1.0 / 60.0);
assert!(r.settled, "a paused layout must report settled for redraw purposes");
assert_eq!(r.max_displacement, 0.0);
}
assert_eq!(particles, before, "a paused layout must not move any particle");
assert!(mode.is_settled());
mode.set_paused(false);
let r = mode.tick(&t, &mut particles, 1.0 / 60.0);
assert_ne!(particles, before, "resuming must let the sim move particles again");
let _ = r;
}
#[test]
fn mode3d_is_deterministic_across_identical_runs() {
let edges = [SimEdge { from: NodeIndex(0), to: NodeIndex(1), weight: 1.0 }, SimEdge { from: NodeIndex(0), to: NodeIndex(2), weight: 1.0 }];
let t = topo(3, &edges);
let mut particles_a = vec![Particle::default(); 3];
let mut mode_a = GraphLayoutMode3D::default();
mode_a.set_kind(LayoutKind::Radial);
mode_a.tick(&t, &mut particles_a, 1.0 / 60.0);
let mut particles_b = vec![Particle::default(); 3];
let mut mode_b = GraphLayoutMode3D::default();
mode_b.set_kind(LayoutKind::Radial);
mode_b.tick(&t, &mut particles_b, 1.0 / 60.0);
assert_eq!(particles_a, particles_b);
}
}