oxideav_scene/render.rs
1//! Renderer traits + a stub implementation.
2//!
3//! The real renderer will land as a separate crate (probably
4//! `oxideav-scene-render`) and depend on the rasteriser + text
5//! shaper. This crate defines the trait surface so downstream code
6//! can write against it today.
7
8use oxideav_core::{Error, Result};
9
10use crate::duration::TimeStamp;
11use crate::ops::ExportOp;
12use crate::scene::Scene;
13
14/// One output tick.
15#[derive(Clone, Debug, Default)]
16pub struct RenderedFrame {
17 /// Optional reconstructed video frame. `None` for audio-only
18 /// intervals or for exporters that don't emit raster.
19 pub video: Option<oxideav_core::VideoFrame>,
20 /// Audio samples for the interval since the previous
21 /// `sample_at` call, at the scene's `sample_rate`. Interleaved
22 /// f32. Always valid (silent if no cue fired).
23 pub audio: Vec<f32>,
24 /// Structured export ops (PDF runs, filter-format operators, …).
25 pub operations: Vec<ExportOp>,
26}
27
28/// Driver trait — the outer render loop.
29pub trait SceneRenderer {
30 /// Prepare internal state for a fresh render. Called before the
31 /// first `render_at` on a new scene or after `seek`.
32 fn prepare(&mut self, scene: &Scene) -> Result<()>;
33
34 /// Render the scene at timestamp `t`. `t` must be monotonically
35 /// non-decreasing between consecutive calls unless `seek` is
36 /// called in between.
37 fn render_at(&mut self, scene: &Scene, t: TimeStamp) -> Result<RenderedFrame>;
38
39 /// Jump to `t` — invalidates any per-object state the renderer
40 /// cached from the previous position.
41 fn seek(&mut self, t: TimeStamp) -> Result<()>;
42}
43
44/// Per-object sampler — one instance per `SceneObject`. Implementations
45/// hold the object's decoder / glyph cache / bitmap handle and emit
46/// the concrete pixel contribution for a given timestamp. Plug into a
47/// [`SceneRenderer`] by registering a factory keyed on the object's
48/// `ObjectKind` discriminant.
49pub trait SceneSampler {
50 /// Sample at time `t`. Returns `None` if the object has nothing
51 /// to emit at this timestamp (e.g. a `Video` object whose next
52 /// keyframe is still in the future).
53 fn sample(&mut self, t: TimeStamp) -> Result<Option<SampleOutput>>;
54
55 /// Natural size of the object before `Transform` is applied. Used
56 /// for aspect-ratio-preserving layout in groups.
57 fn natural_size(&self) -> (f32, f32);
58}
59
60/// What a [`SceneSampler`] hands back. Video frames go into the
61/// compositor; text / raw ops flow through to the export pipeline.
62#[non_exhaustive]
63#[derive(Clone, Debug)]
64pub enum SampleOutput {
65 Frame(oxideav_core::VideoFrame),
66 Audio(Vec<f32>),
67 /// Carry-through for vector / structured exports (PDF).
68 StructuredOp(ExportOp),
69}
70
71/// Placeholder implementation. Every call returns
72/// `Error::Unsupported` — included so downstream code can compile
73/// against the trait today.
74#[derive(Default)]
75pub struct StubRenderer;
76
77impl SceneRenderer for StubRenderer {
78 fn prepare(&mut self, _scene: &Scene) -> Result<()> {
79 Err(Error::unsupported(
80 "oxideav-scene: renderer is a scaffold — real renderer lands as a separate crate",
81 ))
82 }
83
84 fn render_at(&mut self, _scene: &Scene, _t: TimeStamp) -> Result<RenderedFrame> {
85 Err(Error::unsupported(
86 "oxideav-scene: renderer is a scaffold — real renderer lands as a separate crate",
87 ))
88 }
89
90 fn seek(&mut self, _t: TimeStamp) -> Result<()> {
91 Err(Error::unsupported(
92 "oxideav-scene: renderer is a scaffold — real renderer lands as a separate crate",
93 ))
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn stub_renderer_returns_unsupported() {
103 let scene = Scene::default();
104 let mut r = StubRenderer;
105 let err = r.prepare(&scene).unwrap_err();
106 assert!(matches!(err, Error::Unsupported(_)));
107 let err = r.render_at(&scene, 0).unwrap_err();
108 assert!(matches!(err, Error::Unsupported(_)));
109 }
110}