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 }