use std::collections::HashMap;
use crate::node_graph::{ connection::{Connection, ConnectionId}, history::{HistoryAction, HistoryState}, node::{Node, NodeId, NodeState, NodeType, PortPosition}, serialization::SerializedNodeGraph, };
#[derive(Clone, Debug, PartialEq)]
pub struct NodeGraphState {
pub nodes: HashMap<String, NodeState>,
pub connections: Vec<Connection>,
pub selected_node: Option<String>,
pub selected_connection: Option<ConnectionId>,
pub zoom: f64,
pub pan: (f64, f64),
}
impl Default for NodeGraphState {
fn default() -> Self {
Self::new()
}
}
impl NodeGraphState {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
connections: Vec::new(),
selected_node: None,
selected_connection: None,
zoom: 1.0,
pan: (0.0, 0.0),
}
}
pub fn add_node(&mut self, state: NodeState) {
self.nodes.insert(state.id.clone(), state);
}
pub fn remove_node(&mut self, id: &str) -> Option<NodeState> {
self.nodes.remove(id)
}
pub fn get_node(&self, id: &str) -> Option<&NodeState> {
self.nodes.get(id)
}
pub fn get_node_mut(&mut self, id: &str) -> Option<&mut NodeState> {
self.nodes.get_mut(id)
}
pub fn update_node_position(&mut self, id: &str, x: f64, y: f64) -> bool {
if let Some(node) = self.nodes.get_mut(id) {
node.position = (x, y);
true
} else {
false
}
}
pub fn add_connection(&mut self, connection: Connection) {
self.connections.push(connection);
}
pub fn remove_connection(&mut self, id: &ConnectionId) -> Option<Connection> {
let pos = self.connections.iter().position(|c| &c.id == id)?;
Some(self.connections.remove(pos))
}
pub fn select_node(&mut self, id: Option<String>) {
if let Some(current_id) = &self.selected_node {
if let Some(node) = self.nodes.get_mut(current_id) {
node.selected = false;
}
}
self.selected_node = id.clone();
if let Some(new_id) = id {
if let Some(node) = self.nodes.get_mut(&new_id) {
node.selected = true;
}
}
}
pub fn select_connection(&mut self, id: Option<ConnectionId>) {
self.selected_connection = id;
}
pub fn set_zoom(&mut self, zoom: f64, min: f64, max: f64) {
self.zoom = zoom.clamp(min, max);
}
pub fn zoom_in(&mut self, factor: f64, min: f64, max: f64) {
self.set_zoom(self.zoom * factor, min, max);
}
pub fn zoom_out(&mut self, factor: f64, min: f64, max: f64) {
self.set_zoom(self.zoom / factor, min, max);
}
pub fn reset_view(&mut self) {
self.zoom = 1.0;
self.pan = (0.0, 0.0);
}
pub fn pan(&mut self, dx: f64, dy: f64) {
self.pan.0 += dx;
self.pan.1 += dy;
}
pub fn calculate_port_position(
&self,
node_id: &str,
_port_id: &str,
port_position: PortPosition,
) -> Option<(f64, f64)> {
let node_state = self.nodes.get(node_id)?;
let (node_x, node_y) = node_state.position;
let (node_width, node_height) = node_state.size;
let (port_x, port_y) = match port_position {
PortPosition::Top => (node_x + node_width / 2.0, node_y),
PortPosition::Bottom => (node_x + node_width / 2.0, node_y + node_height),
PortPosition::Left => (node_x, node_y + node_height / 2.0),
PortPosition::Right => (node_x + node_width, node_y + node_height / 2.0),
};
Some((port_x, port_y))
}
pub fn transform_style(&self) -> String {
format!(
"transform: scale({}) translate({}px, {}px);",
self.zoom, self.pan.0, self.pan.1
)
}
pub fn clear(&mut self) {
self.nodes.clear();
self.connections.clear();
self.selected_node = None;
self.selected_connection = None;
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct NodeGraphCanvasConfig {
pub width: f64,
pub height: f64,
pub min_zoom: f64,
pub max_zoom: f64,
pub show_minimap: bool,
pub show_controls: bool,
pub grid_size: f64,
}
impl Default for NodeGraphCanvasConfig {
fn default() -> Self {
Self {
width: 1200.0,
height: 800.0,
min_zoom: 0.1,
max_zoom: 3.0,
show_minimap: true,
show_controls: true,
grid_size: 20.0,
}
}
}
impl NodeGraphCanvasConfig {
pub fn with_size(mut self, width: f64, height: f64) -> Self {
self.width = width;
self.height = height;
self
}
pub fn with_zoom_bounds(mut self, min: f64, max: f64) -> Self {
self.min_zoom = min;
self.max_zoom = max;
self
}
pub fn with_minimap(mut self, show: bool) -> Self {
self.show_minimap = show;
self
}
pub fn with_controls(mut self, show: bool) -> Self {
self.show_controls = show;
self
}
pub fn container_style(&self) -> String {
format!("width: {}px; height: {}px;", self.width, self.height)
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum NodeGraphEvent {
NodeAdded { id: String, node_type: NodeType, position: (f64, f64) },
NodeSelected(String),
NodeMoved { id: String, to: (f64, f64) },
NodeDeleted(String),
ConnectionCreated { id: ConnectionId, from_node: String, from_port: String, to_node: String, to_port: String },
ConnectionDeleted(ConnectionId),
ZoomChanged(f64),
Panned { dx: f64, dy: f64 },
Undo,
Redo,
Save,
Load,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_graph_state_new() {
let state = NodeGraphState::new();
assert!(state.nodes.is_empty());
assert!(state.connections.is_empty());
assert_eq!(state.zoom, 1.0);
assert_eq!(state.pan, (0.0, 0.0));
}
#[test]
fn test_add_remove_node() {
let mut state = NodeGraphState::new();
let node = NodeState::new("node1".to_string());
state.add_node(node.clone());
assert_eq!(state.nodes.len(), 1);
assert!(state.get_node("node1").is_some());
let removed = state.remove_node("node1");
assert!(removed.is_some());
assert!(state.nodes.is_empty());
}
#[test]
fn test_update_node_position() {
let mut state = NodeGraphState::new();
state.add_node(NodeState::new("node1".to_string()));
assert!(state.update_node_position("node1", 100.0, 200.0));
assert_eq!(state.get_node("node1").unwrap().position, (100.0, 200.0));
}
#[test]
fn test_select_node() {
let mut state = NodeGraphState::new();
state.add_node(NodeState::new("node1".to_string()));
state.add_node(NodeState::new("node2".to_string()));
state.select_node(Some("node1".to_string()));
assert_eq!(state.selected_node, Some("node1".to_string()));
assert!(state.get_node("node1").unwrap().selected);
state.select_node(Some("node2".to_string()));
assert_eq!(state.selected_node, Some("node2".to_string()));
assert!(!state.get_node("node1").unwrap().selected);
assert!(state.get_node("node2").unwrap().selected);
}
#[test]
fn test_zoom() {
let mut state = NodeGraphState::new();
state.set_zoom(2.0, 0.1, 3.0);
assert_eq!(state.zoom, 2.0);
state.set_zoom(5.0, 0.1, 3.0);
assert_eq!(state.zoom, 3.0);
state.zoom_in(1.5, 0.1, 3.0);
assert_eq!(state.zoom, 3.0);
state.zoom_out(2.0, 0.1, 3.0);
assert_eq!(state.zoom, 1.5); }
#[test]
fn test_pan() {
let mut state = NodeGraphState::new();
state.pan(10.0, 20.0);
assert_eq!(state.pan, (10.0, 20.0));
state.pan(-5.0, -10.0);
assert_eq!(state.pan, (5.0, 10.0));
}
#[test]
fn test_reset_view() {
let mut state = NodeGraphState::new();
state.zoom = 2.0;
state.pan = (100.0, 200.0);
state.reset_view();
assert_eq!(state.zoom, 1.0);
assert_eq!(state.pan, (0.0, 0.0));
}
#[test]
fn test_calculate_port_position() {
let mut state = NodeGraphState::new();
let node = NodeState::new("node1".to_string())
.with_position(100.0, 100.0)
.with_size(200.0, 150.0);
state.add_node(node);
let pos = state.calculate_port_position("node1", "port1", PortPosition::Top);
assert_eq!(pos, Some((200.0, 100.0)));
let pos = state.calculate_port_position("node1", "port1", PortPosition::Right);
assert_eq!(pos, Some((300.0, 175.0)));
let pos = state.calculate_port_position("node1", "port1", PortPosition::Bottom);
assert_eq!(pos, Some((200.0, 250.0)));
let pos = state.calculate_port_position("node1", "port1", PortPosition::Left);
assert_eq!(pos, Some((100.0, 175.0)));
}
#[test]
fn test_transform_style() {
let state = NodeGraphState {
zoom: 1.5,
pan: (10.0, 20.0),
..Default::default()
};
let style = state.transform_style();
assert!(style.contains("scale(1.5)"));
assert!(style.contains("translate(10px"));
assert!(style.contains("20px"));
}
#[test]
fn test_config_default() {
let config = NodeGraphCanvasConfig::default();
assert_eq!(config.width, 1200.0);
assert_eq!(config.height, 800.0);
assert_eq!(config.min_zoom, 0.1);
assert_eq!(config.max_zoom, 3.0);
}
#[test]
fn test_config_builder() {
let config = NodeGraphCanvasConfig::default()
.with_size(800.0, 600.0)
.with_zoom_bounds(0.5, 2.0)
.with_minimap(false)
.with_controls(false);
assert_eq!(config.width, 800.0);
assert_eq!(config.height, 600.0);
assert_eq!(config.min_zoom, 0.5);
assert_eq!(config.max_zoom, 2.0);
assert!(!config.show_minimap);
assert!(!config.show_controls);
}
}