Skip to main content

lumen_engine/node/
mod.rs

1//! Renderer-agnostic node schema and shared parameter types.
2//!
3//! Node structs stay intentionally small here: they describe graph shape and
4//! animatable parameters. GPU lowering lives in `crate::gpu`.
5
6use std::fmt;
7
8use crate::{
9    error::{LumenError, PropertyError},
10    expr::Expression,
11};
12
13pub mod compositing;
14pub mod media_output;
15pub mod processing;
16pub mod source;
17pub mod vector;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct NodeId(pub u64);
21
22impl NodeId {
23    pub const fn new(value: u64) -> Self {
24        Self(value)
25    }
26}
27
28impl fmt::Display for NodeId {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "{}", self.0)
31    }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub struct TrackId(pub u64);
36
37impl TrackId {
38    pub const fn new(value: u64) -> Self {
39        Self(value)
40    }
41}
42
43impl fmt::Display for TrackId {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}", self.0)
46    }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct InputPortDef {
51    pub name: &'static str,
52    pub kind: PortKind,
53    pub optional: bool,
54    pub variadic: bool,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct OutputPortDef {
59    pub name: &'static str,
60    pub kind: PortKind,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64#[repr(u8)]
65pub enum PropertyKind {
66    Float = 0,
67    Int = 1,
68    Bool = 2,
69    String = 3,
70    Color = 4,
71    Vec2 = 5,
72    Enum = 6,
73}
74
75#[cfg(any(feature = "json", feature = "metadata"))]
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct EnumOptionDef {
78    pub name: &'static str,
79    pub label: &'static str,
80    pub value: i64,
81}
82
83#[cfg(any(feature = "json", feature = "metadata"))]
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct EnumDef {
86    pub name: &'static str,
87    pub options: &'static [EnumOptionDef],
88}
89
90#[cfg(any(feature = "json", feature = "metadata"))]
91pub trait NodeEnum {
92    fn enum_def() -> &'static EnumDef;
93}
94
95#[derive(Debug, Clone, Copy, PartialEq)]
96pub struct PropertyDef {
97    pub id: &'static str,
98    pub expected: PropertyKind,
99    #[cfg(any(feature = "json", feature = "metadata"))]
100    pub enum_def: Option<&'static EnumDef>,
101    #[cfg(feature = "metadata")]
102    pub name: &'static str,
103    #[cfg(feature = "metadata")]
104    pub description: &'static str,
105    #[cfg(feature = "metadata")]
106    pub constraints: PropertyConstraints,
107}
108
109#[cfg(feature = "metadata")]
110#[derive(Debug, Clone, Copy, Default, PartialEq)]
111pub struct PropertyConstraints {
112    pub min: Option<f64>,
113    pub max: Option<f64>,
114    pub step: Option<f64>,
115    pub format: Option<&'static str>,
116    pub multiline: bool,
117    pub recommended_rows: Option<u32>,
118    pub role: Option<&'static str>,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122#[repr(u8)]
123pub enum NodeCategory {
124    Compositing = 0,
125    Processing = 1,
126    Source = 2,
127    Output = 3,
128    Vector = 4,
129}
130
131#[derive(Debug, Clone)]
132pub struct NodeSchemaDef {
133    pub kind: &'static str,
134    pub name: &'static str,
135    pub description: &'static str,
136    pub category: NodeCategory,
137    pub inputs: &'static [InputPortDef],
138    pub properties: Vec<PropertyDef>,
139    pub default_properties: Vec<(&'static str, NodeProperty)>,
140}
141
142#[cfg(feature = "metadata")]
143pub trait NodeSchema: Default {
144    fn schema() -> NodeSchemaDef;
145}
146
147#[cfg(feature = "json")]
148pub trait JsonNode: Default {
149    fn from_json(
150        id: NodeId,
151        properties: Option<&serde_json::Map<String, serde_json::Value>>,
152    ) -> anyhow::Result<Self>
153    where
154        Self: Sized;
155
156    fn set_input_json(&mut self, port: &str, source: PortRef) -> anyhow::Result<()>;
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
160#[repr(u8)]
161pub enum PortKind {
162    Raster = 0,
163    Vector = 1,
164}
165
166#[derive(Debug, Clone)]
167pub enum NodeProperty {
168    Float(f64),
169    Int(i64),
170    Bool(bool),
171    String(String),
172    Color([u8; 4]),
173    Vec2((f64, f64)),
174    FloatVec(Vec<f64>),
175    IntVec(Vec<i64>),
176    StringVec(Vec<String>),
177    Expr(Expression),
178}
179
180impl NodeProperty {
181    fn invalid_type(
182        node_id: NodeId,
183        property_path: &str,
184        expected: &'static str,
185        actual: &'static str,
186    ) -> LumenError {
187        LumenError::Property(PropertyError::InvalidType {
188            node_id,
189            property_path: property_path.to_string(),
190            expected,
191            actual,
192        })
193    }
194
195    pub fn resolve_float(
196        &self,
197        node_id: NodeId,
198        property_path: &str,
199        ctx: &crate::expr::ExpressionContext<'_>,
200    ) -> crate::Result<f64> {
201        match self {
202            Self::Float(value) => Ok(*value),
203            Self::Int(value) => Ok(*value as f64),
204            Self::String(value) => value
205                .parse::<f64>()
206                .map_err(|_| Self::invalid_type(node_id, property_path, "Float", "String")),
207            Self::Expr(expr) => expr
208                .evaluate(ctx)?
209                .as_f64()
210                .ok_or_else(|| Self::invalid_type(node_id, property_path, "Float", "expression")),
211            _ => Err(Self::invalid_type(
212                node_id,
213                property_path,
214                "Float",
215                "unsupported",
216            )),
217        }
218    }
219
220    pub fn resolve_int(
221        &self,
222        node_id: NodeId,
223        property_path: &str,
224        ctx: &crate::expr::ExpressionContext<'_>,
225    ) -> crate::Result<i64> {
226        match self {
227            Self::Int(value) => Ok(*value),
228            Self::Float(value) => Ok(*value as i64),
229            Self::Bool(value) => Ok(i64::from(*value)),
230            Self::String(value) => value
231                .parse::<i64>()
232                .map_err(|_| Self::invalid_type(node_id, property_path, "Int", "String")),
233            Self::Expr(expr) => {
234                let value = expr.evaluate(ctx)?.as_f64().ok_or_else(|| {
235                    Self::invalid_type(node_id, property_path, "Int", "expression")
236                })?;
237                Ok(value as i64)
238            }
239            _ => Err(Self::invalid_type(
240                node_id,
241                property_path,
242                "Int",
243                "unsupported",
244            )),
245        }
246    }
247
248    pub fn resolve_bool(
249        &self,
250        node_id: NodeId,
251        property_path: &str,
252        ctx: &crate::expr::ExpressionContext<'_>,
253    ) -> crate::Result<bool> {
254        match self {
255            Self::Bool(value) => Ok(*value),
256            Self::Int(value) => Ok(*value != 0),
257            Self::Float(value) => Ok(*value != 0.0),
258            Self::String(value) => match value.to_ascii_lowercase().as_str() {
259                "true" | "1" => Ok(true),
260                "false" | "0" => Ok(false),
261                _ => Err(Self::invalid_type(node_id, property_path, "Bool", "String")),
262            },
263            Self::Expr(expr) => Ok(expr.evaluate(ctx)?.as_bool()),
264            _ => Err(Self::invalid_type(
265                node_id,
266                property_path,
267                "Bool",
268                "unsupported",
269            )),
270        }
271    }
272
273    pub fn resolve_string(
274        &self,
275        node_id: NodeId,
276        property_path: &str,
277        ctx: &crate::expr::ExpressionContext<'_>,
278    ) -> crate::Result<String> {
279        match self {
280            Self::String(value) => Ok(value.clone()),
281            Self::Int(value) => Ok(value.to_string()),
282            Self::Float(value) => Ok(value.to_string()),
283            Self::Bool(value) => Ok(value.to_string()),
284            Self::Expr(expr) => Ok(expr.evaluate(ctx)?.as_string()),
285            _ => Err(Self::invalid_type(
286                node_id,
287                property_path,
288                "String",
289                "unsupported",
290            )),
291        }
292    }
293
294    pub fn resolve_color(
295        &self,
296        node_id: NodeId,
297        property_path: &str,
298        _ctx: &crate::expr::ExpressionContext<'_>,
299    ) -> crate::Result<[u8; 4]> {
300        match self {
301            Self::Color(value) => Ok(*value),
302            _ => Err(Self::invalid_type(
303                node_id,
304                property_path,
305                "Color",
306                "unsupported",
307            )),
308        }
309    }
310
311    pub fn resolve_vec2(
312        &self,
313        node_id: NodeId,
314        property_path: &str,
315        _ctx: &crate::expr::ExpressionContext<'_>,
316    ) -> crate::Result<(f64, f64)> {
317        match self {
318            Self::Vec2(value) => Ok(*value),
319            _ => Err(Self::invalid_type(
320                node_id,
321                property_path,
322                "Vec2",
323                "unsupported",
324            )),
325        }
326    }
327}
328
329pub trait Node: Send + Sync {
330    fn id(&self) -> NodeId;
331    fn input_port_defs(&self) -> &'static [InputPortDef];
332    fn output_port_defs(&self) -> &'static [OutputPortDef] {
333        SINGLE_RASTER_OUTPUT
334    }
335}
336
337pub trait PropertyEval {
338    fn get_property(&self, id: &str) -> crate::Result<Option<NodeProperty>>;
339}
340
341pub const SINGLE_RASTER_OUTPUT: &[OutputPortDef] = &[OutputPortDef {
342    name: "output",
343    kind: PortKind::Raster,
344}];
345
346#[derive(Debug, Clone, PartialEq, Eq, Hash)]
347pub struct PortRef {
348    pub id: NodeId,
349    pub port: String,
350}
351
352impl PortRef {
353    pub fn new(id: NodeId, port: String) -> Self {
354        Self { id, port }
355    }
356
357    pub fn empty() -> Self {
358        Self {
359            id: NodeId::new(0),
360            port: String::new(),
361        }
362    }
363
364    pub fn is_empty(&self) -> bool {
365        self.id.0 == 0
366    }
367}
368
369#[derive(Debug)]
370pub enum NodeKind {
371    MediaIn(source::media_in::MediaIn),
372    SolidColor(source::solid_color::SolidColor),
373    Text(source::text::Text),
374    Path(vector::path::Path),
375    Shape(vector::shape::Shape),
376    Boolean(compositing::boolean::Boolean),
377    Merge(compositing::merge::Merge),
378    RasterMultiMerge(compositing::raster_multimerge::RasterMultiMerge),
379    AlphaPremultiply(processing::alpha_premultiply::AlphaPremultiply),
380    Blur(processing::blur::Blur),
381    ChannelShuffle(processing::channel_shuffle::ChannelShuffle),
382    ColorGrade(processing::color_grade::ColorGrade),
383    Curves(processing::curves::Curves),
384    Exposure(processing::exposure::Exposure),
385    HueSaturation(processing::hue_saturation::HueSaturation),
386    Levels(processing::levels::Levels),
387    Memo(processing::memo::Memo),
388    TimeRemap(processing::time_remap::TimeRemap),
389    Transform(processing::transform::Transform),
390    Crop(processing::crop::Crop),
391    Resize(processing::resize::Resize),
392    Shadow(processing::shadow::Shadow),
393    WgslShader(processing::wgsl_shader::WgslShader),
394    Switch(compositing::switch::Switch),
395    MediaOutput(media_output::MediaOutput),
396}
397
398impl NodeKind {
399    pub fn id(&self) -> NodeId {
400        match self {
401            Self::MediaIn(node) => node.id,
402            Self::SolidColor(node) => node.id,
403            Self::Text(node) => node.id,
404            Self::Path(node) => node.id,
405            Self::Shape(node) => node.id,
406            Self::Boolean(node) => node.id,
407            Self::Merge(node) => node.id,
408            Self::RasterMultiMerge(node) => node.id,
409            Self::AlphaPremultiply(node) => node.id,
410            Self::Blur(node) => node.id,
411            Self::ChannelShuffle(node) => node.id,
412            Self::ColorGrade(node) => node.id,
413            Self::Curves(node) => node.id,
414            Self::Exposure(node) => node.id,
415            Self::HueSaturation(node) => node.id,
416            Self::Levels(node) => node.id,
417            Self::Memo(node) => node.id,
418            Self::TimeRemap(node) => node.id,
419            Self::Transform(node) => node.id,
420            Self::Crop(node) => node.id,
421            Self::Resize(node) => node.id,
422            Self::Shadow(node) => node.id,
423            Self::WgslShader(node) => node.id,
424            Self::Switch(node) => node.id,
425            Self::MediaOutput(node) => node.id,
426        }
427    }
428
429    pub fn as_property_eval(&self) -> &dyn PropertyEval {
430        self
431    }
432
433    #[cfg(feature = "metadata")]
434    pub fn schemas() -> Vec<NodeSchemaDef> {
435        vec![
436            source::media_in::MediaIn::schema(),
437            source::solid_color::SolidColor::schema(),
438            source::text::Text::schema(),
439            vector::path::Path::schema(),
440            vector::shape::Shape::schema(),
441            compositing::boolean::Boolean::schema(),
442            compositing::merge::Merge::schema(),
443            compositing::raster_multimerge::RasterMultiMerge::schema(),
444            compositing::switch::Switch::schema(),
445            processing::memo::Memo::schema(),
446            processing::alpha_premultiply::AlphaPremultiply::schema(),
447            processing::blur::Blur::schema(),
448            processing::channel_shuffle::ChannelShuffle::schema(),
449            processing::color_grade::ColorGrade::schema(),
450            processing::curves::Curves::schema(),
451            processing::exposure::Exposure::schema(),
452            processing::hue_saturation::HueSaturation::schema(),
453            processing::levels::Levels::schema(),
454            processing::time_remap::TimeRemap::schema(),
455            processing::transform::Transform::schema(),
456            processing::crop::Crop::schema(),
457            processing::resize::Resize::schema(),
458            processing::shadow::Shadow::schema(),
459            processing::wgsl_shader::WgslShader::schema(),
460            media_output::MediaOutput::schema(),
461        ]
462    }
463}
464
465impl Node for NodeKind {
466    fn id(&self) -> NodeId {
467        self.id()
468    }
469
470    fn input_port_defs(&self) -> &'static [InputPortDef] {
471        match self {
472            Self::MediaIn(node) => node.input_port_defs(),
473            Self::SolidColor(node) => node.input_port_defs(),
474            Self::Text(node) => node.input_port_defs(),
475            Self::Path(node) => node.input_port_defs(),
476            Self::Shape(node) => node.input_port_defs(),
477            Self::Boolean(node) => node.input_port_defs(),
478            Self::Merge(node) => node.input_port_defs(),
479            Self::RasterMultiMerge(node) => node.input_port_defs(),
480            Self::AlphaPremultiply(node) => node.input_port_defs(),
481            Self::Blur(node) => node.input_port_defs(),
482            Self::ChannelShuffle(node) => node.input_port_defs(),
483            Self::ColorGrade(node) => node.input_port_defs(),
484            Self::Curves(node) => node.input_port_defs(),
485            Self::Exposure(node) => node.input_port_defs(),
486            Self::HueSaturation(node) => node.input_port_defs(),
487            Self::Levels(node) => node.input_port_defs(),
488            Self::Memo(node) => node.input_port_defs(),
489            Self::TimeRemap(node) => node.input_port_defs(),
490            Self::Transform(node) => node.input_port_defs(),
491            Self::Crop(node) => node.input_port_defs(),
492            Self::Resize(node) => node.input_port_defs(),
493            Self::Shadow(node) => node.input_port_defs(),
494            Self::WgslShader(node) => node.input_port_defs(),
495            Self::Switch(node) => node.input_port_defs(),
496            Self::MediaOutput(node) => node.input_port_defs(),
497        }
498    }
499
500    fn output_port_defs(&self) -> &'static [OutputPortDef] {
501        SINGLE_RASTER_OUTPUT
502    }
503}
504
505impl PropertyEval for NodeKind {
506    fn get_property(&self, id: &str) -> crate::Result<Option<NodeProperty>> {
507        match self {
508            Self::MediaIn(node) => node.get_property(id),
509            Self::SolidColor(node) => node.get_property(id),
510            Self::Text(node) => node.get_property(id),
511            Self::Path(node) => node.get_property(id),
512            Self::Shape(node) => node.get_property(id),
513            Self::Boolean(node) => node.get_property(id),
514            Self::Merge(node) => node.get_property(id),
515            Self::RasterMultiMerge(node) => node.get_property(id),
516            Self::AlphaPremultiply(node) => node.get_property(id),
517            Self::Blur(node) => node.get_property(id),
518            Self::ChannelShuffle(node) => node.get_property(id),
519            Self::ColorGrade(node) => node.get_property(id),
520            Self::Curves(node) => node.get_property(id),
521            Self::Exposure(node) => node.get_property(id),
522            Self::HueSaturation(node) => node.get_property(id),
523            Self::Levels(node) => node.get_property(id),
524            Self::Memo(node) => node.get_property(id),
525            Self::TimeRemap(node) => node.get_property(id),
526            Self::Transform(node) => node.get_property(id),
527            Self::Crop(node) => node.get_property(id),
528            Self::Resize(node) => node.get_property(id),
529            Self::Shadow(node) => node.get_property(id),
530            Self::WgslShader(node) => node.get_property(id),
531            Self::Switch(node) => node.get_property(id),
532            Self::MediaOutput(node) => node.get_property(id),
533        }
534    }
535}