playa/entities/node.rs
1//! Node trait - base interface for all node types in the project.
2//!
3//! Nodes are the building blocks of the compositing graph:
4//! - FileNode: loads image sequences/video from disk
5//! - CompNode: composites multiple layers
6//!
7//! Each node can compute a frame at given time, has attributes,
8//! and participates in dirty tracking for efficient caching.
9//!
10//! ## Play Range Helpers
11//!
12//! For timeline bounds and work area, see [`NodeKind`](super::node_kind::NodeKind):
13//! - `play_range(use_work_area)` → `(start, end)` frame range
14//! - `bounds(use_trim, selection_only)` → content bounds
15//! - `frame_count()` → total frames
16
17use enum_dispatch::enum_dispatch;
18use std::sync::Arc;
19use uuid::Uuid;
20
21use crate::config::{DEFAULT_DIM, DEFAULT_FPS, DEFAULT_SRC_LEN};
22
23use super::attrs::Attrs;
24use super::frame::Frame;
25use super::keys::{
26 A_FPS, A_FRAME, A_HEIGHT, A_IN, A_OUT, A_SRC_LEN, A_TRIM_IN, A_TRIM_OUT, A_WIDTH,
27};
28use super::traits::{FrameCache, WorkerPool};
29
30/// Context passed to node compute and preload functions.
31/// Contains references to project resources needed for computation.
32///
33/// ## Why Arc<NodeKind> in media?
34///
35/// Workers need read access during compute (50-500ms), but UI needs write
36/// access for playhead updates. Without Arc, workers block UI with read locks.
37///
38/// With Arc<NodeKind>:
39/// - Workers take snapshot (clone HashMap of Arcs) in microseconds
40/// - Lock released immediately, UI never blocked
41/// - Compute uses owned snapshot, safe from concurrent mutation
42pub struct ComputeContext<'a> {
43 /// Global frame cache (trait object for dependency inversion)
44 pub cache: &'a dyn FrameCache,
45 /// Cache Arc for worker thread access in preload (clone this for workers)
46 pub cache_arc: Option<Arc<dyn FrameCache + Send + Sync>>,
47 /// Media pool for looking up source nodes.
48 /// Values are Arc<NodeKind> for cheap cloning - workers snapshot this
49 /// and release lock before expensive compute operations.
50 pub media: &'a std::collections::HashMap<Uuid, Arc<super::node_kind::NodeKind>>,
51 /// Media pool Arc for worker thread access in preload.
52 /// Workers clone this, take snapshot of inner HashMap, then release lock.
53 pub media_arc: Option<std::sync::Arc<std::sync::RwLock<std::collections::HashMap<Uuid, Arc<super::node_kind::NodeKind>>>>>,
54 /// Worker pool for background loading (trait object, None during synchronous compute)
55 pub workers: Option<&'a dyn WorkerPool>,
56 /// Current epoch for cancelling stale preload requests
57 pub epoch: u64,
58}
59
60/// Base trait for all node types.
61/// Provides common interface for identification, attributes, and computation.
62#[enum_dispatch]
63pub trait Node: Send + Sync {
64 /// Unique identifier for this node
65 fn uuid(&self) -> Uuid;
66
67 /// Display name of the node
68 fn name(&self) -> &str;
69
70 /// Type identifier string ("File", "Comp", etc.)
71 fn node_type(&self) -> &'static str;
72
73 /// Access to node's persistent attributes
74 fn attrs(&self) -> &Attrs;
75
76 /// Mutable access to node's attributes
77 fn attrs_mut(&mut self) -> &mut Attrs;
78
79 /// Source nodes that this node depends on (via layers).
80 /// Empty for leaf nodes like FileNode.
81 fn inputs(&self) -> Vec<Uuid>;
82
83 /// Compute output frame at given frame index.
84 /// Result should be cached in global_cache[uuid][frame].
85 /// Returns None if computation fails or no frame available.
86 fn compute(&self, frame: i32, ctx: &ComputeContext) -> Option<Frame>;
87
88 /// Check if node needs recomputation (attrs changed).
89 ///
90 /// # Arguments
91 /// - `ctx`: If Some, also checks source nodes recursively. If None, checks only self.
92 fn is_dirty(&self, ctx: Option<&ComputeContext>) -> bool;
93
94 /// Mark node as needing recomputation
95 fn mark_dirty(&self);
96
97 /// Clear dirty flag after successful computation
98 fn clear_dirty(&self);
99
100 /// Preload frames around center position for background loading.
101 /// Default implementation is no-op (for nodes without preload support).
102 /// FileNode/CompNode override this to enqueue frame loading via workers.
103 /// `radius` - max number of frames to preload around center
104 fn preload(&self, _center: i32, _radius: i32, _ctx: &ComputeContext) {
105 // Default no-op
106 }
107
108 // --- Convenience methods with default implementations ---
109
110 /// Get attribute value by key
111 fn get_attr(&self, key: &str) -> Option<&super::attrs::AttrValue> {
112 self.attrs().get(key)
113 }
114
115 /// Set attribute value
116 fn set_attr(&mut self, key: &str, value: super::attrs::AttrValue) {
117 self.attrs_mut().set(key, value);
118 }
119
120 /// Get i32 attribute
121 fn get_i32(&self, key: &str) -> Option<i32> {
122 self.attrs().get_i32(key)
123 }
124
125 /// Get f32 attribute
126 fn get_float(&self, key: &str) -> Option<f32> {
127 self.attrs().get_float(key)
128 }
129
130 /// Get string attribute
131 fn get_str(&self, key: &str) -> Option<&str> {
132 self.attrs().get_str(key)
133 }
134
135 /// Get uuid attribute
136 fn get_uuid_attr(&self, key: &str) -> Option<Uuid> {
137 self.attrs().get_uuid(key)
138 }
139
140 // --- Timeline/timing methods (for enum_dispatch unification) ---
141
142 /// Play range: (start_frame, end_frame) for playback.
143 /// Default uses attrs.layer_start()/layer_end() which respects in/trim/speed.
144 fn play_range(&self, _use_work_area: bool) -> (i32, i32) {
145 (self.attrs().layer_start(), self.attrs().layer_end())
146 }
147
148 /// Content bounds for zoom-to-fit. Default delegates to play_range.
149 /// CompNode overrides to calculate from layers with dynamic src_len from media.
150 fn bounds(&self, use_trim: bool, _selection_only: bool, _media: &std::collections::HashMap<Uuid, Arc<super::node_kind::NodeKind>>) -> (i32, i32) {
151 self.play_range(use_trim)
152 }
153
154 // --- Timing methods with defaults ---
155
156 /// Start frame (in point). Default: 0
157 fn _in(&self) -> i32 {
158 self.attrs().get_i32(A_IN).unwrap_or(0)
159 }
160
161 /// End frame (out point). Default: src_len or DEFAULT_SRC_LEN
162 fn _out(&self) -> i32 {
163 self.attrs().get_i32(A_OUT).unwrap_or_else(|| {
164 self.attrs().get_i32(A_SRC_LEN).unwrap_or(DEFAULT_SRC_LEN)
165 })
166 }
167
168 /// Frames per second. Default: DEFAULT_FPS (24.0)
169 fn fps(&self) -> f32 {
170 self.attrs().get_float(A_FPS).unwrap_or(DEFAULT_FPS)
171 }
172
173 /// Current playhead frame. Default: _in()
174 fn frame(&self) -> i32 {
175 self.attrs().get_i32(A_FRAME).unwrap_or_else(|| self._in())
176 }
177
178 /// Work area (trimmed range) in absolute frames.
179 /// Returns (in + trim_in, out - trim_out)
180 fn work_area(&self) -> (i32, i32) {
181 let trim_in = self.attrs().get_i32(A_TRIM_IN).unwrap_or(0);
182 let trim_out = self.attrs().get_i32(A_TRIM_OUT).unwrap_or(0);
183 (self._in() + trim_in, self._out() - trim_out)
184 }
185
186 /// Total source frames: out - in + 1 (inclusive range)
187 fn frame_count(&self) -> i32 {
188 (self._out() - self._in() + 1).max(0)
189 }
190
191 /// Play frame count (respects trims): work_area duration
192 fn play_frame_count(&self) -> i32 {
193 let (start, end) = self.work_area();
194 (end - start + 1).max(0)
195 }
196
197 /// Dimensions (width, height). Default: DEFAULT_DIM (1920x1080)
198 fn dim(&self) -> (usize, usize) {
199 let w = self.attrs().get_u32(A_WIDTH).unwrap_or(DEFAULT_DIM.0 as u32) as usize;
200 let h = self.attrs().get_u32(A_HEIGHT).unwrap_or(DEFAULT_DIM.1 as u32) as usize;
201 (w.max(1), h.max(1))
202 }
203
204 /// Placeholder frame with node dimensions
205 fn placeholder_frame(&self) -> Frame {
206 let (w, h) = self.dim();
207 Frame::placeholder(w, h)
208 }
209}