use super::GraphInfo;
use super::camera::Camera2D;
use super::euclid::WorldPoint;
use iced::{Point, keyboard, touch};
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use web_time::Instant;
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) enum Dragging {
#[default]
None,
Graph(WorldPoint), Node(usize, WorldPoint), Edge(usize, usize, WorldPoint), EdgeOver(usize, usize, usize, usize), BoxSelect(WorldPoint, WorldPoint), GroupMove(WorldPoint), EdgeCutting {
trail: Vec<WorldPoint>,
pending_cuts: HashSet<usize>,
},
}
#[derive(Debug)]
pub(super) struct NodeGraphState {
pub(super) camera: Camera2D,
pub(super) dragging: Dragging,
pub(super) time: f32,
pub(super) last_update: Option<Instant>,
pub(super) selected_nodes: HashSet<usize>,
pub(super) last_synced_external: Option<HashSet<usize>>,
pub(super) modifiers: keyboard::Modifiers,
pub(super) valid_drop_targets: HashSet<(usize, usize)>,
pub(super) last_synced_view: Option<(Point, f32)>,
pub(super) sdf_animated: Cell<bool>,
pub(super) last_info: RefCell<Option<GraphInfo>>,
pub(super) node_z: HashMap<usize, u64>,
pub(super) z_counter: u64,
pub(super) fingers: Vec<(touch::Finger, Point)>,
pub(super) touch_tap: Option<(touch::Finger, Point, f32)>,
}
impl Default for NodeGraphState {
fn default() -> Self {
Self {
camera: Camera2D::new(),
dragging: Default::default(),
time: 0.0,
last_update: None,
selected_nodes: HashSet::new(),
last_synced_external: None,
modifiers: keyboard::Modifiers::default(),
valid_drop_targets: HashSet::new(),
last_synced_view: None,
sdf_animated: Cell::new(false),
last_info: RefCell::new(None),
node_z: HashMap::new(),
z_counter: 0,
fingers: Vec::new(),
touch_tap: None,
}
}
}
impl NodeGraphState {
pub(super) fn ensure_z_entries(&mut self, node_count: usize) {
for idx in 0..node_count {
if let std::collections::hash_map::Entry::Vacant(e) = self.node_z.entry(idx) {
e.insert(self.z_counter);
self.z_counter = self.z_counter.wrapping_add(1);
}
}
}
pub(super) fn promote_z(&mut self, idx: usize) {
self.node_z.insert(idx, self.z_counter);
self.z_counter = self.z_counter.wrapping_add(1);
}
pub(super) fn promote_z_many(&mut self, indices: &[usize]) {
let mut sorted: Vec<usize> = indices.to_vec();
sorted.sort_by_key(|i| self.node_z.get(i).copied().unwrap_or(0));
for idx in sorted {
self.promote_z(idx);
}
}
}
pub(super) fn z_render_indices(state: &NodeGraphState, node_count: usize) -> Vec<usize> {
let mut indices: Vec<usize> = (0..node_count).collect();
indices.sort_by_key(|&i| {
let selected = state.selected_nodes.contains(&i);
let z = state.node_z.get(&i).copied().unwrap_or(0);
(selected, z)
});
indices
}
#[cfg(test)]
mod tests {
use super::*;
use euclid::Point2D;
#[test]
fn test_dragging_default_is_none() {
let dragging: Dragging = Default::default();
assert_eq!(dragging, Dragging::None);
}
#[test]
fn test_dragging_states_not_equal() {
let origin = Point2D::new(10.0, 20.0);
assert_ne!(Dragging::None, Dragging::Graph(origin));
assert_ne!(Dragging::Graph(origin), Dragging::Node(0, origin));
assert_ne!(Dragging::Node(0, origin), Dragging::Edge(0, 0, origin));
}
#[test]
fn test_dragging_graph_stores_origin() {
let origin = Point2D::new(100.0, 200.0);
let dragging = Dragging::Graph(origin);
if let Dragging::Graph(stored) = dragging {
assert_eq!(stored.x, 100.0);
assert_eq!(stored.y, 200.0);
} else {
panic!("Expected Dragging::Graph");
}
}
#[test]
fn test_dragging_node_stores_index_and_origin() {
let origin = Point2D::new(50.0, 75.0);
let dragging = Dragging::Node(5, origin);
if let Dragging::Node(idx, stored) = dragging {
assert_eq!(idx, 5);
assert_eq!(stored.x, 50.0);
assert_eq!(stored.y, 75.0);
} else {
panic!("Expected Dragging::Node");
}
}
#[test]
fn test_dragging_edge_stores_node_pin_and_cursor() {
let cursor = Point2D::new(300.0, 400.0);
let dragging = Dragging::Edge(2, 1, cursor);
if let Dragging::Edge(node, pin, stored) = dragging {
assert_eq!(node, 2);
assert_eq!(pin, 1);
assert_eq!(stored.x, 300.0);
assert_eq!(stored.y, 400.0);
} else {
panic!("Expected Dragging::Edge");
}
}
#[test]
fn test_box_select_stores_two_points() {
let start = Point2D::new(0.0, 0.0);
let current = Point2D::new(100.0, 100.0);
let dragging = Dragging::BoxSelect(start, current);
if let Dragging::BoxSelect(s, c) = dragging {
assert_eq!(s.x, 0.0);
assert_eq!(s.y, 0.0);
assert_eq!(c.x, 100.0);
assert_eq!(c.y, 100.0);
} else {
panic!("Expected Dragging::BoxSelect");
}
}
#[test]
fn test_group_move_stores_origin() {
let origin = Point2D::new(250.0, 350.0);
let dragging = Dragging::GroupMove(origin);
if let Dragging::GroupMove(stored) = dragging {
assert_eq!(stored.x, 250.0);
assert_eq!(stored.y, 350.0);
} else {
panic!("Expected Dragging::GroupMove");
}
}
#[test]
fn test_edge_cutting_trail() {
let trail = vec![
Point2D::new(0.0, 0.0),
Point2D::new(10.0, 10.0),
Point2D::new(20.0, 20.0),
];
let mut pending_cuts = HashSet::new();
pending_cuts.insert(1);
pending_cuts.insert(3);
let dragging = Dragging::EdgeCutting {
trail: trail.clone(),
pending_cuts: pending_cuts.clone(),
};
if let Dragging::EdgeCutting {
trail: stored,
pending_cuts: cuts,
} = dragging
{
assert_eq!(stored.len(), 3);
assert_eq!(stored[0].x, 0.0);
assert_eq!(stored[2].x, 20.0);
assert!(cuts.contains(&1));
assert!(cuts.contains(&3));
assert!(!cuts.contains(&2));
} else {
panic!("Expected Dragging::EdgeCutting");
}
}
#[test]
fn test_selection_set_operations() {
let mut state = NodeGraphState::default();
assert!(state.selected_nodes.is_empty());
state.selected_nodes.insert(0);
state.selected_nodes.insert(2);
state.selected_nodes.insert(5);
assert_eq!(state.selected_nodes.len(), 3);
assert!(state.selected_nodes.contains(&0));
assert!(state.selected_nodes.contains(&2));
assert!(state.selected_nodes.contains(&5));
assert!(!state.selected_nodes.contains(&1));
state.selected_nodes.remove(&2);
assert_eq!(state.selected_nodes.len(), 2);
assert!(!state.selected_nodes.contains(&2));
state.selected_nodes.clear();
assert!(state.selected_nodes.is_empty());
}
#[test]
fn test_node_graph_state_default() {
let state = NodeGraphState::default();
assert_eq!(state.dragging, Dragging::None);
assert_eq!(state.time, 0.0);
assert!(state.last_update.is_none());
assert!(state.selected_nodes.is_empty());
assert!(state.valid_drop_targets.is_empty());
assert!(state.node_z.is_empty());
assert_eq!(state.z_counter, 0);
}
#[test]
fn test_ensure_z_entries_assigns_new_indices() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(3);
assert_eq!(state.node_z.get(&0), Some(&0));
assert_eq!(state.node_z.get(&1), Some(&1));
assert_eq!(state.node_z.get(&2), Some(&2));
assert_eq!(state.z_counter, 3);
state.ensure_z_entries(3);
assert_eq!(state.z_counter, 3);
state.ensure_z_entries(5);
assert_eq!(state.node_z.get(&3), Some(&3));
assert_eq!(state.node_z.get(&4), Some(&4));
}
#[test]
fn test_promote_z_puts_node_on_top() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(3);
state.promote_z(0);
let z0 = state.node_z[&0];
let z1 = state.node_z[&1];
let z2 = state.node_z[&2];
assert!(z0 > z1);
assert!(z0 > z2);
}
#[test]
fn test_promote_z_many_preserves_relative_order() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(4);
state.promote_z_many(&[0, 2]);
assert!(state.node_z[&0] > state.node_z[&1]);
assert!(state.node_z[&0] > state.node_z[&3]);
assert!(state.node_z[&2] > state.node_z[&0]);
}
#[test]
fn test_z_render_indices_unselected_then_selected() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(4);
state.promote_z(1);
state.selected_nodes.insert(3);
let order = z_render_indices(&state, 4);
assert_eq!(order.last(), Some(&3));
let one_pos = order.iter().position(|&i| i == 1).unwrap();
assert_eq!(one_pos, 2);
}
#[test]
fn test_z_render_indices_selected_sorted_by_z() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(3);
state.selected_nodes.insert(0);
state.selected_nodes.insert(2);
let order = z_render_indices(&state, 3);
assert_eq!(order, vec![1, 0, 2]);
}
}