use crate::core::id::NodeId;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub fn distance(&self, other: &Point) -> f32 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
fn add(&self, other: &Point) -> Point {
Point::new(self.x + other.x, self.y + other.y)
}
fn sub(&self, other: &Point) -> Point {
Point::new(self.x - other.x, self.y - other.y)
}
fn scale(&self, factor: f32) -> Point {
Point::new(self.x * factor, self.y * factor)
}
fn magnitude(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
fn normalize(&self) -> Point {
let mag = self.magnitude();
if mag == 0.0 {
Point::new(0.0, 0.0)
} else {
self.scale(1.0 / mag)
}
}
}
#[derive(Debug, Clone)]
pub struct LayoutConfig {
pub optimal_distance: f32,
pub repulsion_strength: f32,
pub spring_strength: f32,
pub semantic_strength: f32,
pub cooling_factor: f32,
pub iterations: usize,
pub width: f32,
pub height: f32,
}
impl Default for LayoutConfig {
fn default() -> Self {
Self {
optimal_distance: 50.0,
repulsion_strength: 1.0,
spring_strength: 0.1,
semantic_strength: 100.0, cooling_factor: 0.95,
iterations: 100,
width: 1000.0,
height: 1000.0,
}
}
}
pub struct LayoutEngine {
config: LayoutConfig,
nodes: Vec<NodeId>,
positions: HashMap<NodeId, Point>,
velocities: HashMap<NodeId, Point>,
edges: Vec<(NodeId, NodeId)>,
semantic_links: HashMap<(NodeId, NodeId), f32>,
temperature: f32,
}
impl LayoutEngine {
pub fn new(config: LayoutConfig) -> Self {
let width = config.width;
Self {
temperature: width / 10.0, config,
nodes: Vec::new(),
positions: HashMap::new(),
velocities: HashMap::new(),
edges: Vec::new(),
semantic_links: HashMap::new(),
}
}
pub fn add_node(&mut self, id: NodeId) {
if !self.positions.contains_key(&id) {
self.nodes.push(id);
let seed = id.as_u64();
let x = ((seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407))
% 1000) as f32;
let y = ((seed
.wrapping_mul(1442695040888963407)
.wrapping_add(6364136223846793005))
% 1000) as f32;
self.positions.insert(id, Point::new(x, y));
self.velocities.insert(id, Point::new(0.0, 0.0));
}
}
pub fn add_edge(&mut self, a: NodeId, b: NodeId) {
self.add_node(a);
self.add_node(b);
self.edges.push((a, b));
}
pub fn add_semantic_link(&mut self, a: NodeId, b: NodeId, similarity: f32) {
if similarity > 0.0 {
self.add_node(a);
self.add_node(b);
let key = if a < b { (a, b) } else { (b, a) };
self.semantic_links.insert(key, similarity);
}
}
pub fn run(&mut self) {
for _ in 0..self.config.iterations {
self.step();
if self.temperature < 0.1 {
break;
}
}
}
pub fn step(&mut self) {
let k = self.config.optimal_distance;
let mut forces: HashMap<NodeId, Point> = self
.nodes
.iter()
.map(|&id| (id, Point::new(0.0, 0.0)))
.collect();
for i in 0..self.nodes.len() {
let u = self.nodes[i];
let pos_u = self.positions[&u];
for j in (i + 1)..self.nodes.len() {
let v = self.nodes[j];
let pos_v = self.positions[&v];
let delta = pos_u.sub(&pos_v);
let dist = delta.magnitude();
let dist = if dist < 0.1 { 0.1 } else { dist };
let repulsion = (k * k) / dist * self.config.repulsion_strength;
let force_vec = delta.normalize().scale(repulsion);
let f_u = forces.get_mut(&u).unwrap();
*f_u = f_u.add(&force_vec);
let f_v = forces.get_mut(&v).unwrap();
*f_v = f_v.sub(&force_vec);
}
}
for &(u, v) in &self.edges {
let pos_u = self.positions[&u];
let pos_v = self.positions[&v];
let delta = pos_u.sub(&pos_v);
let dist = delta.magnitude();
let attraction = (dist * dist) / k * self.config.spring_strength;
let force_vec = delta.normalize().scale(attraction);
let f_u = forces.get_mut(&u).unwrap();
*f_u = f_u.sub(&force_vec);
let f_v = forces.get_mut(&v).unwrap();
*f_v = f_v.add(&force_vec);
}
for (&(u, v), &similarity) in &self.semantic_links {
let pos_u = self.positions[&u];
let pos_v = self.positions[&v];
let delta = pos_u.sub(&pos_v);
let gravity = similarity * self.config.semantic_strength;
let force_vec = delta.normalize().scale(gravity);
let f_u = forces.get_mut(&u).unwrap();
*f_u = f_u.sub(&force_vec);
let f_v = forces.get_mut(&v).unwrap();
*f_v = f_v.add(&force_vec);
}
for &id in &self.nodes {
let force = forces[&id];
let mag = force.magnitude();
let limit = self.temperature;
let movement = if mag > limit {
force.normalize().scale(limit)
} else {
force
};
let pos = self.positions.get_mut(&id).unwrap();
*pos = pos.add(&movement);
}
self.temperature *= self.config.cooling_factor;
}
pub fn get_positions(&self) -> &HashMap<NodeId, Point> {
&self.positions
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_topology_convergence() {
let config = LayoutConfig {
iterations: 50,
..LayoutConfig::default()
};
let mut engine = LayoutEngine::new(config);
let n1 = NodeId::new(1).unwrap();
let n2 = NodeId::new(2).unwrap();
let n3 = NodeId::new(3).unwrap();
engine.add_edge(n1, n2);
engine.add_node(n3);
engine.run();
let pos = engine.get_positions();
let p1 = pos[&n1];
let p2 = pos[&n2];
let p3 = pos[&n3];
let d12 = p1.distance(&p2);
let d13 = p1.distance(&p3);
assert!(
d12 < d13,
"Connected nodes (dist={}) should be closer than unconnected (dist={})",
d12,
d13
);
}
#[test]
fn test_semantic_gravity() {
let config = LayoutConfig {
iterations: 50,
semantic_strength: 200.0, ..LayoutConfig::default()
};
let mut engine = LayoutEngine::new(config);
let n1 = NodeId::new(1).unwrap();
let n2 = NodeId::new(2).unwrap(); let n3 = NodeId::new(3).unwrap();
engine.add_node(n1);
engine.add_node(n2);
engine.add_node(n3);
engine.add_semantic_link(n1, n2, 1.0);
engine.run();
let pos = engine.get_positions();
let p1 = pos[&n1];
let p2 = pos[&n2];
let p3 = pos[&n3];
let d12 = p1.distance(&p2);
let d13 = p1.distance(&p3);
assert!(
d12 < d13,
"Semantically similar nodes (dist={}) should be closer than others (dist={})",
d12,
d13
);
}
#[test]
fn test_visual_output() {
let config = LayoutConfig {
width: 100.0,
height: 40.0,
iterations: 100,
..LayoutConfig::default()
};
let mut engine = LayoutEngine::new(config);
let center = NodeId::new(1).unwrap();
for i in 2..=6 {
let sat = NodeId::new(i).unwrap();
engine.add_edge(center, sat);
}
let c2_1 = NodeId::new(10).unwrap();
let c2_2 = NodeId::new(11).unwrap();
let c2_3 = NodeId::new(12).unwrap();
engine.add_edge(c2_1, c2_2);
engine.add_edge(c2_2, c2_3);
engine.add_edge(c2_3, c2_1);
engine.run();
let pos = engine.get_positions();
let w = 80;
let h = 20;
let mut grid = vec![vec![' '; w]; h];
let mut min_x = f32::MAX;
let mut max_x = f32::MIN;
let mut min_y = f32::MAX;
let mut max_y = f32::MIN;
for p in pos.values() {
if p.x < min_x {
min_x = p.x;
}
if p.x > max_x {
max_x = p.x;
}
if p.y < min_y {
min_y = p.y;
}
if p.y > max_y {
max_y = p.y;
}
}
let range_x = max_x - min_x;
let range_y = max_y - min_y;
println!("\nKaleidoscope Layout:");
println!(
"Bounds: X[{:.1}, {:.1}] Y[{:.1}, {:.1}]",
min_x, max_x, min_y, max_y
);
for (id, p) in pos {
let gx = ((p.x - min_x) / range_x * (w as f32 - 1.0)) as usize;
let gy = ((p.y - min_y) / range_y * (h as f32 - 1.0)) as usize;
if gx < w && gy < h {
let c = if id.as_u64() == 1 {
'*'
} else if id.as_u64() >= 10 {
'#'
} else {
'o'
};
grid[gy][gx] = c;
}
}
println!("+{}+", "-".repeat(w));
for row in grid {
let s: String = row.into_iter().collect();
println!("|{}|", s);
}
println!("+{}+", "-".repeat(w));
println!("Legend: * = Center, o = Satellite, # = Cluster 2");
}
}