Skip to main content

flutmax_sema/
graph.rs

1/// Structure representing the entire patch graph.
2/// A collection of nodes (Max objects) and edges (patch cords).
3#[derive(Debug, Clone)]
4pub struct PatchGraph {
5    pub nodes: Vec<PatchNode>,
6    pub edges: Vec<PatchEdge>,
7}
8
9/// Node purity classification.
10/// Whether an object has internal state (stateful) or depends only on inputs (pure).
11#[derive(Debug, Clone, PartialEq)]
12pub enum NodePurity {
13    /// Depends only on inputs (cycle~, +~, biquad~, etc.)
14    Pure,
15    /// Has internal state (pack, int, float, toggle, counter, etc.)
16    Stateful,
17    /// No information in objdb, or unclassifiable
18    Unknown,
19}
20
21/// A single node (Max object) in the patch graph.
22#[derive(Debug, Clone)]
23pub struct PatchNode {
24    pub id: String,
25    pub object_name: String,
26    pub args: Vec<String>,
27    pub num_inlets: u32,
28    pub num_outlets: u32,
29    /// Whether this is a Signal object (name ends with `~`).
30    /// Signal object fanouts don't need trigger insertion.
31    pub is_signal: bool,
32    /// flutmax wire name. Output as Max's varname attribute.
33    /// None for inlets/outlets and auto-inserted triggers.
34    pub varname: Option<String>,
35    /// Whether each inlet is hot. hot_inlets[i] = true means inlet i is hot.
36    /// Empty means unset (default: inlet 0 is hot, others are cold).
37    pub hot_inlets: Vec<bool>,
38    /// Purity classification of the object.
39    pub purity: NodePurity,
40    /// Attributes specified via `.attr()` chain. Vector of key-value pairs.
41    /// Added as `@key value` to newobj text in codegen,
42    /// or output as top-level fields in box JSON for UI objects.
43    pub attrs: Vec<(String, String)>,
44    /// Inline code for codebox. Used by v8.codebox / codebox (gen~).
45    /// Output as the `code` field in .maxpat JSON.
46    pub code: Option<String>,
47}
48
49/// A single edge (patch cord) in the patch graph.
50#[derive(Debug, Clone)]
51pub struct PatchEdge {
52    pub source_id: String,
53    pub source_outlet: u32,
54    pub dest_id: String,
55    pub dest_inlet: u32,
56    /// Whether this is a feedback edge (cyclic edge between tapin~ and tapout~).
57    /// Feedback edges are excluded from topological sort and trigger insertion.
58    pub is_feedback: bool,
59    /// Edge order for fanouts.
60    /// Assigned 0, 1, 2... when multiple edges share the same (source_id, source_outlet).
61    /// None for single edges.
62    pub order: Option<u32>,
63}
64
65impl PatchGraph {
66    /// Create an empty patch graph.
67    pub fn new() -> Self {
68        PatchGraph {
69            nodes: Vec::new(),
70            edges: Vec::new(),
71        }
72    }
73
74    /// Add a node and return a reference to its ID.
75    pub fn add_node(&mut self, node: PatchNode) -> &str {
76        self.nodes.push(node);
77        &self.nodes.last().unwrap().id
78    }
79
80    /// Add an edge.
81    pub fn add_edge(&mut self, edge: PatchEdge) {
82        self.edges.push(edge);
83    }
84
85    /// Find a node by ID.
86    pub fn find_node(&self, id: &str) -> Option<&PatchNode> {
87        self.nodes.iter().find(|n| n.id == id)
88    }
89
90    /// Find a node by ID with mutable reference.
91    pub fn find_node_mut(&mut self, id: &str) -> Option<&mut PatchNode> {
92        self.nodes.iter_mut().find(|n| n.id == id)
93    }
94}
95
96impl Default for PatchGraph {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_node_purity_equality() {
108        assert_eq!(NodePurity::Pure, NodePurity::Pure);
109        assert_eq!(NodePurity::Stateful, NodePurity::Stateful);
110        assert_eq!(NodePurity::Unknown, NodePurity::Unknown);
111        assert_ne!(NodePurity::Pure, NodePurity::Stateful);
112        assert_ne!(NodePurity::Pure, NodePurity::Unknown);
113    }
114
115    #[test]
116    fn test_patch_edge_order_default() {
117        let edge = PatchEdge {
118            source_id: "a".into(),
119            source_outlet: 0,
120            dest_id: "b".into(),
121            dest_inlet: 0,
122            is_feedback: false,
123            order: None,
124        };
125        assert_eq!(edge.order, None);
126    }
127
128    #[test]
129    fn test_patch_edge_order_set() {
130        let edge = PatchEdge {
131            source_id: "a".into(),
132            source_outlet: 0,
133            dest_id: "b".into(),
134            dest_inlet: 0,
135            is_feedback: false,
136            order: Some(2),
137        };
138        assert_eq!(edge.order, Some(2));
139    }
140
141    #[test]
142    fn test_patch_node_hot_inlets() {
143        let node = PatchNode {
144            id: "test".into(),
145            object_name: "cycle~".into(),
146            args: vec![],
147            num_inlets: 2,
148            num_outlets: 1,
149            is_signal: true,
150            varname: None,
151            hot_inlets: vec![true, false],
152            purity: NodePurity::Pure,
153            attrs: vec![],
154            code: None,
155        };
156        assert!(node.hot_inlets[0]);
157        assert!(!node.hot_inlets[1]);
158        assert_eq!(node.purity, NodePurity::Pure);
159    }
160}