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