git.lucas.co / cce-graph
node-based graph editor
git clone https://git.lucas.co/cce-graph.git

src/wiring.rs (5K)

  1 //! Node identity and mouse-drawn wires for the project editor.
  2 //!
  3 //! The `Graph` widget reports a wire drawn with the mouse as a pending
  4 //! connection naming the downstream node by `id` and the upstream one by
  5 //! `name`, and draws wires from parameters (`cce_ui::widget::node_wires`):
  6 //! a node's `node`-typed parameters, one per input port in order, else its
  7 //! parameter named `input`. Two things were missing, so every drawn wire
  8 //! was dropped: nodes loaded and added with an empty id (the pending
  9 //! connection could not say which node), and nothing took the pending
 10 //! connection and wrote it into a parameter.
 11 
 12 use cce_ui::widget::GraphNode;
 13 
 14 /// Give every node without one a unique session id. Ids are not saved:
 15 /// wires reference names, so a fresh id per load is enough.
 16 pub fn ensure_ids(nodes: &mut [GraphNode]) {
 17     let mut taken: std::collections::HashSet<String> =
 18         nodes.iter().filter(|n| !n.id.is_empty()).map(|n| n.id.clone()).collect();
 19     let mut next = 1usize;
 20     for node in nodes.iter_mut().filter(|n| n.id.is_empty()) {
 21         while taken.contains(&format!("n{next}")) {
 22             next += 1;
 23         }
 24         node.id = format!("n{next}");
 25         taken.insert(node.id.clone());
 26     }
 27 }
 28 
 29 /// Wire `from` (a node name) into input `port` of the node with id `to`.
 30 /// Writes the value of that port's parameter — the port-th `node`-typed
 31 /// one, or the `input` parameter for port 0 of a node that has no typed
 32 /// ones — and adds the parameter when the node has none for that port.
 33 /// False when no node has that id, or the wire would loop onto itself.
 34 pub fn connect(nodes: &mut [GraphNode], to: &str, from: &str, port: usize) -> bool {
 35     let Some(node) = nodes.iter_mut().find(|n| n.id == to) else { return false };
 36     if node.name == from {
 37         return false;
 38     }
 39     let is_typed = |p: &(String, String, String)| p.2 == "node";
 40     let untyped_input = node.parameters.iter().position(|p| p.0.eq_ignore_ascii_case("input"));
 41     if !node.parameters.iter().any(is_typed) {
 42         if let Some(i) = untyped_input {
 43             if port == 0 {
 44                 node.parameters[i].1 = from.to_string();
 45                 return true;
 46             }
 47             // A later port on a node whose only wire is an untyped
 48             // `input`: type that one, so it stays port 0.
 49             node.parameters[i].2 = "node".into();
 50         }
 51     }
 52     let typed: Vec<usize> =
 53         node.parameters.iter().enumerate().filter(|(_, p)| is_typed(p)).map(|(i, _)| i).collect();
 54     if let Some(&i) = typed.get(port) {
 55         node.parameters[i].1 = from.to_string();
 56         return true;
 57     }
 58     // Typed parameters are read in order, so pad up to this port.
 59     for k in typed.len()..=port {
 60         let name = if k == 0 { "input".to_string() } else { format!("input{}", k + 1) };
 61         let value = if k == port { from.to_string() } else { String::new() };
 62         node.parameters.push((name, value, "node".into()));
 63     }
 64     true
 65 }
 66 
 67 #[cfg(test)]
 68 mod tests {
 69     use super::*;
 70     use cce_ui::widget::node_wires;
 71 
 72     fn node(id: &str, name: &str, params: Vec<(&str, &str, &str)>) -> GraphNode {
 73         GraphNode {
 74             id: id.into(),
 75             name: name.into(),
 76             position: (0.0, 0.0),
 77             parameters: params.into_iter().map(|(a, b, c)| (a.into(), b.into(), c.into())).collect(),
 78             geom_visible: true,
 79             node_type: String::new(),
 80             inputs: 2,
 81             outputs: 1,
 82         }
 83     }
 84 
 85     #[test]
 86     fn ids_are_unique_and_kept() {
 87         let mut nodes = vec![node("", "a", vec![]), node("n1", "b", vec![]), node("", "c", vec![])];
 88         ensure_ids(&mut nodes);
 89         let ids: Vec<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
 90         assert_eq!(ids, ["n2", "n1", "n3"]);
 91     }
 92 
 93     #[test]
 94     fn a_drawn_wire_becomes_the_port_parameter() {
 95         let mut nodes = vec![node("n1", "a", vec![]), node("n2", "b", vec![])];
 96         assert!(connect(&mut nodes, "n2", "a", 0));
 97         assert_eq!(node_wires(&nodes[1]), ["a"]);
 98         // Port 1 pads nothing (port 0 exists) and adds the second input.
 99         assert!(connect(&mut nodes, "n2", "a", 1));
100         assert_eq!(node_wires(&nodes[1]), ["a", "a"]);
101         // Rewiring port 0 replaces it.
102         nodes.push(node("n3", "c", vec![]));
103         assert!(connect(&mut nodes, "n2", "c", 0));
104         assert_eq!(node_wires(&nodes[1]), ["c", "a"]);
105         assert!(!connect(&mut nodes, "n9", "a", 0));
106         assert!(!connect(&mut nodes, "n1", "a", 0), "no self-loop");
107     }
108 
109     #[test]
110     fn an_untyped_input_parameter_keeps_working() {
111         let mut nodes = vec![node("n1", "x", vec![("Input", "old", "string")])];
112         assert!(connect(&mut nodes, "n1", "y", 0));
113         assert_eq!(node_wires(&nodes[0]), ["y"]);
114         assert!(connect(&mut nodes, "n1", "z", 1));
115         assert_eq!(node_wires(&nodes[0]), ["y", "z"]);
116     }
117 
118     #[test]
119     fn a_later_port_pads_the_ones_before_it() {
120         let mut nodes = vec![node("n1", "x", vec![])];
121         assert!(connect(&mut nodes, "n1", "y", 1));
122         assert_eq!(node_wires(&nodes[0]), ["", "y"]);
123     }
124 }