playa/entities/
node_kind.rs

1//! NodeKind - enum wrapper for all node types.
2//!
3//! Provides unified interface for storing different node types
4//! in Project.media HashMap.
5
6use enum_dispatch::enum_dispatch;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use uuid::Uuid;
10
11use super::attrs::Attrs;
12use super::camera_node::CameraNode;
13use super::comp_node::CompNode;
14use super::file_node::FileNode;
15use super::frame::Frame;
16use super::node::{ComputeContext, Node};
17use super::text_node::TextNode;
18
19/// Enum containing all possible node types.
20/// Used in Project.media for unified storage.
21#[enum_dispatch(Node)]
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub enum NodeKind {
24    File(FileNode),
25    Comp(CompNode),
26    Camera(CameraNode),
27    Text(TextNode),
28}
29
30impl NodeKind {
31    /// Check if this is a file node
32    pub fn is_file(&self) -> bool {
33        matches!(self, NodeKind::File(_))
34    }
35    
36    /// Check if this is a comp node
37    pub fn is_comp(&self) -> bool {
38        matches!(self, NodeKind::Comp(_))
39    }
40    
41    /// Check if this is a camera node
42    pub fn is_camera(&self) -> bool {
43        matches!(self, NodeKind::Camera(_))
44    }
45    
46    /// Check if this is a text node
47    pub fn is_text(&self) -> bool {
48        matches!(self, NodeKind::Text(_))
49    }
50
51    /// Check if this node type can be rendered as a layer.
52    /// Returns false for control nodes (camera, light, null, audio).
53    pub fn is_renderable(&self) -> bool {
54        match self {
55            NodeKind::Camera(_) => false,
56            // Future: Light, Transform (null), Audio -> false
57            _ => true,
58        }
59    }
60
61    /// Check if listed in Project UI (false = hidden preview comp)
62    pub fn is_listed(&self) -> bool {
63        self.attrs().get_bool("listed").unwrap_or(true)
64    }
65
66    // play_range, bounds, frame_count, dim - now via Node trait (enum_dispatch)
67
68    /// Add child layer (only works on CompNode)
69    ///
70    /// # initial_position parameter
71    ///
72    /// Optional starting position for the layer. Used for cameras which need
73    /// Z=-1000 to be pulled back from the scene (at Z=0 they'd see nothing).
74    /// Regular layers use None → default [0,0,0].
75    pub fn add_child_layer(
76        &mut self,
77        source_uuid: Uuid,
78        name: &str,
79        start_frame: i32,
80        duration: i32,
81        insert_idx: Option<usize>,
82        source_dim: (usize, usize),
83        renderable: bool,
84        initial_position: Option<[f32; 3]>,
85    ) -> anyhow::Result<Uuid> {
86        match self {
87            NodeKind::Comp(comp) => comp.add_child_layer(source_uuid, name, start_frame, duration, insert_idx, source_dim, renderable, initial_position),
88            _ => anyhow::bail!("Cannot add child to non-Comp node"),
89        }
90    }
91    
92    /// Get as FileNode reference
93    pub fn as_file(&self) -> Option<&FileNode> {
94        match self {
95            NodeKind::File(n) => Some(n),
96            _ => None,
97        }
98    }
99    
100    /// Get as FileNode mutable reference
101    pub fn as_file_mut(&mut self) -> Option<&mut FileNode> {
102        match self {
103            NodeKind::File(n) => Some(n),
104            _ => None,
105        }
106    }
107    
108    /// Get as CompNode reference
109    pub fn as_comp(&self) -> Option<&CompNode> {
110        match self {
111            NodeKind::Comp(n) => Some(n),
112            _ => None,
113        }
114    }
115    
116    /// Get as CompNode mutable reference
117    pub fn as_comp_mut(&mut self) -> Option<&mut CompNode> {
118        match self {
119            NodeKind::Comp(n) => Some(n),
120            _ => None,
121        }
122    }
123    
124    /// Get as CameraNode reference
125    pub fn as_camera(&self) -> Option<&CameraNode> {
126        match self {
127            NodeKind::Camera(n) => Some(n),
128            _ => None,
129        }
130    }
131    
132    /// Get as CameraNode mutable reference
133    pub fn as_camera_mut(&mut self) -> Option<&mut CameraNode> {
134        match self {
135            NodeKind::Camera(n) => Some(n),
136            _ => None,
137        }
138    }
139    
140    /// Get as TextNode reference
141    pub fn as_text(&self) -> Option<&TextNode> {
142        match self {
143            NodeKind::Text(n) => Some(n),
144            _ => None,
145        }
146    }
147    
148    /// Get as TextNode mutable reference
149    pub fn as_text_mut(&mut self) -> Option<&mut TextNode> {
150        match self {
151            NodeKind::Text(n) => Some(n),
152            _ => None,
153        }
154    }
155    
156    /// Check if this is a file-mode node (FileNode = true, CompNode = false)
157    pub fn is_file_mode(&self) -> bool {
158        matches!(self, NodeKind::File(_))
159    }
160    
161    /// Get FPS
162    pub fn fps(&self) -> f32 {
163        match self {
164            NodeKind::File(n) => n.fps(),
165            NodeKind::Comp(n) => n.fps(),
166            NodeKind::Camera(_) => 24.0, // Default
167            NodeKind::Text(_) => 24.0,   // Default
168        }
169    }
170    
171    /// Get file mask (only for FileNode)
172    pub fn file_mask(&self) -> Option<String> {
173        match self {
174            NodeKind::File(n) => n.file_mask().map(|s| s.to_string()),
175            _ => None,
176        }
177    }
178    
179    /// Get start frame (_in)
180    pub fn _in(&self) -> i32 {
181        match self {
182            NodeKind::File(n) => n._in(),
183            NodeKind::Comp(n) => n._in(),
184            NodeKind::Camera(n) => n._in(),
185            NodeKind::Text(n) => n._in(),
186        }
187    }
188    
189    /// Get end frame (_out)
190    pub fn _out(&self) -> i32 {
191        match self {
192            NodeKind::File(n) => n._out(),
193            NodeKind::Comp(n) => n._out(),
194            NodeKind::Camera(n) => n._out(),
195            NodeKind::Text(n) => n._out(),
196        }
197    }
198    
199    /// Get current frame (playhead)
200    pub fn frame(&self) -> i32 {
201        match self {
202            NodeKind::File(n) => n.frame(),
203            NodeKind::Comp(n) => n.frame(),
204            NodeKind::Camera(n) => n.frame(),
205            NodeKind::Text(n) => n.frame(),
206        }
207    }
208    
209    /// Set event emitter (only affects CompNode)
210    pub fn set_event_emitter(&mut self, emitter: crate::core::event_bus::CompEventEmitter) {
211        if let NodeKind::Comp(n) = self {
212            n.set_event_emitter(emitter);
213        }
214    }
215}
216// Node trait impl and From<T> for NodeKind are auto-generated by enum_dispatch
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    
222    #[test]
223    fn test_node_kind_file() {
224        let file = FileNode::new("test.*.exr".to_string(), 1, 100, 24.0);
225        let kind: NodeKind = file.into();
226        
227        assert!(kind.is_file());
228        assert!(!kind.is_comp());
229        assert_eq!(kind.node_type(), "File");
230    }
231    
232    #[test]
233    fn test_node_kind_comp() {
234        let comp = CompNode::new("Test Comp", 0, 100, 24.0);
235        let kind: NodeKind = comp.into();
236        
237        assert!(!kind.is_file());
238        assert!(kind.is_comp());
239        assert_eq!(kind.node_type(), "Comp");
240    }
241    
242    #[test]
243    fn test_node_trait_delegation() {
244        let file = FileNode::new("test.*.exr".to_string(), 1, 100, 24.0);
245        let file_uuid = file.uuid();
246        let kind: NodeKind = file.into();
247        
248        assert_eq!(kind.uuid(), file_uuid);
249        assert!(kind.inputs().is_empty());
250    }
251}