Skip to main content

lumen_engine/node/processing/
time_remap.rs

1use crate::node::{NodeId, NodeParamEvalContext, NodeParams, PortRef};
2
3use crate::gpu::{BoundFrame, CompiledOutput, FrameBindContext, GpuCompileNode, GpuCompiledNode};
4
5/// Evaluates a raster input at another frame.
6#[derive(Debug, Clone, Default, lumen_macros::Delegate)]
7pub struct TimeRemapParams {
8    /// Source frame to sample.
9    #[meta(min = 0, step = 1)]
10    pub frame: f64,
11    /// Enables looping between the configured loop bounds.
12    #[meta()]
13    pub loop_enabled: bool,
14    /// First frame in the loop range.
15    #[meta(min = 0, step = 1)]
16    pub loop_start: i64,
17    /// Last frame in the loop range.
18    #[meta(min = 0, step = 1)]
19    pub loop_end: i64,
20}
21
22/// Evaluates a raster input at another frame.
23#[derive(Debug, Clone, lumen_macros::Node)]
24#[node(kind = "time_remap", name = "Time Remap", category = "processing")]
25pub struct TimeRemap {
26    pub id: NodeId,
27    #[params]
28    pub params: TimeRemapParamsDelegate,
29
30    #[input()]
31    pub source: PortRef,
32}
33
34#[derive(Debug, Clone, Copy)]
35pub struct TimeRemapSettings {
36    pub frame: f64,
37    pub loop_enabled: bool,
38    pub loop_start: i64,
39    pub loop_end: i64,
40}
41
42impl Default for TimeRemap {
43    fn default() -> Self {
44        Self {
45            id: NodeId::new(0),
46            params: TimeRemapParamsDelegate::default(),
47            source: PortRef::empty(),
48        }
49    }
50}
51
52pub fn remap_frame(settings: TimeRemapSettings) -> u32 {
53    let mut frame = settings.frame.max(0.0).floor() as i64;
54    if settings.loop_enabled && settings.loop_end > settings.loop_start {
55        let span = settings.loop_end - settings.loop_start;
56        frame = settings.loop_start + (frame - settings.loop_start).rem_euclid(span);
57    }
58    frame.max(0) as u32
59}
60
61impl GpuCompileNode for TimeRemap {
62    fn compile_gpu(
63        &self,
64        ctx: &mut crate::gpu::CompileContext<'_>,
65        port: &PortRef,
66    ) -> crate::Result<CompiledOutput> {
67        if port.port != "output" {
68            return Err(ctx.missing_output(self.id, &port.port));
69        }
70        ctx.register_compiled_node(CompiledTimeRemap {
71            node_id: self.id,
72            params: self.params.clone(),
73        });
74        let params = self.params.eval(&NodeParamEvalContext {
75            node_id: self.id,
76            expr: &ctx.expr_context(self.id, "params"),
77        })?;
78        let target_frame = remap_frame(TimeRemapSettings {
79            frame: params.frame,
80            loop_enabled: params.loop_enabled,
81            loop_start: params.loop_start,
82            loop_end: params.loop_end,
83        });
84        ctx.with_frame_context(target_frame, |ctx| ctx.compile_port(&self.source))
85    }
86}
87
88#[derive(Debug, Clone)]
89struct CompiledTimeRemap {
90    node_id: NodeId,
91    params: TimeRemapParamsDelegate,
92}
93
94impl GpuCompiledNode for CompiledTimeRemap {
95    fn node_id(&self) -> NodeId {
96        self.node_id
97    }
98
99    fn bind(&self, ctx: &FrameBindContext<'_>, _bound: &mut BoundFrame) -> crate::Result<()> {
100        let evaluated = self.params.eval(&NodeParamEvalContext {
101            node_id: self.node_id,
102            expr: &ctx.expr_context(self.node_id, "params"),
103        })?;
104        let _ = remap_frame(TimeRemapSettings {
105            frame: evaluated.frame,
106            loop_enabled: evaluated.loop_enabled,
107            loop_start: evaluated.loop_start,
108            loop_end: evaluated.loop_end,
109        });
110        Ok(())
111    }
112}