Skip to main content

oxideav_scene/
object.rs

1//! Scene objects — what's on the canvas, and where.
2
3use std::sync::Arc;
4
5use crate::animation::Animation;
6use crate::duration::Lifetime;
7use crate::id::ObjectId;
8
9/// Pixel format alias; re-exports [`oxideav_core::PixelFormat`] so
10/// callers don't need a direct core dependency just to build a
11/// canvas.
12pub use oxideav_core::PixelFormat;
13
14/// Canvas — either pixel-based (NLE, compositor) or vector-coord
15/// (PDF pages).
16#[non_exhaustive]
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub enum Canvas {
19    /// Pixel raster. Used by the streaming compositor and the NLE
20    /// timeline.
21    Raster {
22        width: u32,
23        height: u32,
24        pixel_format: PixelFormat,
25    },
26    /// Unit-agnostic vector canvas. PDF pages use this; the unit is
27    /// whatever the producer declared. All scene coordinates live in
28    /// this unit; rasterisation happens at export time.
29    Vector {
30        width: f32,
31        height: f32,
32        unit: LengthUnit,
33    },
34}
35
36impl Canvas {
37    /// Convenience for the common case: 8-bit 4:2:0 raster.
38    pub const fn raster(width: u32, height: u32) -> Self {
39        Canvas::Raster {
40            width,
41            height,
42            pixel_format: PixelFormat::Yuv420P,
43        }
44    }
45
46    /// Pixel dims for raster canvases, `None` for vector canvases.
47    pub fn raster_size(&self) -> Option<(u32, u32)> {
48        match self {
49            Canvas::Raster { width, height, .. } => Some((*width, *height)),
50            Canvas::Vector { .. } => None,
51        }
52    }
53}
54
55/// Length unit for vector canvases.
56#[non_exhaustive]
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub enum LengthUnit {
59    /// PostScript / PDF point: 1/72 inch.
60    #[default]
61    Point,
62    /// Millimetre.
63    Millimetre,
64    /// Inch.
65    Inch,
66    /// CSS pixel (96/in).
67    CssPixel,
68    /// Device pixel — what it is is device-dependent.
69    DevicePixel,
70}
71
72/// One renderable element on a scene.
73#[derive(Clone, Debug)]
74pub struct SceneObject {
75    pub id: ObjectId,
76    pub kind: ObjectKind,
77    pub transform: Transform,
78    pub lifetime: Lifetime,
79    pub animations: Vec<Animation>,
80    pub z_order: i32,
81    pub opacity: f32,
82    pub blend_mode: BlendMode,
83    pub effects: Vec<Effect>,
84    pub clip: Option<ClipRect>,
85}
86
87impl Default for SceneObject {
88    fn default() -> Self {
89        SceneObject {
90            id: ObjectId::default(),
91            kind: ObjectKind::Shape(Shape::rect(0.0, 0.0)),
92            transform: Transform::identity(),
93            lifetime: Lifetime::default(),
94            animations: Vec::new(),
95            z_order: 0,
96            opacity: 1.0,
97            blend_mode: BlendMode::default(),
98            effects: Vec::new(),
99            clip: None,
100        }
101    }
102}
103
104/// What a scene object IS.
105#[non_exhaustive]
106#[derive(Clone, Debug)]
107pub enum ObjectKind {
108    Image(ImageSource),
109    Video(VideoSource),
110    Text(TextRun),
111    Shape(Shape),
112    Group(Vec<ObjectId>),
113    Live(LiveStreamHandle),
114    /// Vector content — a self-contained
115    /// [`oxideav_core::VectorFrame`]. Renders natively to vector
116    /// outputs (PDF / SVG writers consume the `VectorFrame` as-is)
117    /// and rasterises through `oxideav_raster::Renderer` for
118    /// raster outputs (PNG / MP4 / RTMP); see
119    /// [`crate::raster::rasterize_vector`] for the helper. The
120    /// rasteriser also picks up `Group::cache_key` automatically
121    /// when the same sub-tree is re-rendered.
122    Vector(oxideav_core::VectorFrame),
123}
124
125/// Affine placement on the canvas. Applied in this order:
126/// translate → anchor-relative rotate → scale → skew.
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct Transform {
129    pub position: (f32, f32),
130    pub scale: (f32, f32),
131    /// Radians, counter-clockwise, around `anchor`.
132    pub rotation: f32,
133    /// Pivot point in normalised object-local coordinates (0..=1).
134    /// `(0.5, 0.5)` is the object centre.
135    pub anchor: (f32, f32),
136    /// Shear in radians, per axis.
137    pub skew: (f32, f32),
138}
139
140impl Transform {
141    pub const fn identity() -> Self {
142        Transform {
143            position: (0.0, 0.0),
144            scale: (1.0, 1.0),
145            rotation: 0.0,
146            anchor: (0.5, 0.5),
147            skew: (0.0, 0.0),
148        }
149    }
150}
151
152impl Default for Transform {
153    fn default() -> Self {
154        Transform::identity()
155    }
156}
157
158/// Compositing blend — painter's algorithm default is `Normal`.
159#[non_exhaustive]
160#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
161pub enum BlendMode {
162    #[default]
163    Normal,
164    Multiply,
165    Screen,
166    Overlay,
167    Add,
168    /// Subtract destination from source.
169    Subtract,
170    /// Source replaces destination even in transparent regions —
171    /// useful for mask objects.
172    Copy,
173}
174
175/// Filter applied to the object's raster output before compositing.
176/// The parameter map is opaque here; per-effect implementations in
177/// sibling crates interpret it.
178#[derive(Clone, Debug)]
179pub struct Effect {
180    pub name: String,
181    pub params: Vec<(String, f32)>,
182}
183
184/// Axis-aligned clipping rectangle in canvas coordinates.
185#[derive(Clone, Copy, Debug, PartialEq)]
186pub struct ClipRect {
187    pub x: f32,
188    pub y: f32,
189    pub width: f32,
190    pub height: f32,
191}
192
193/// Bitmap source. Either an owned frame, a shared frame handle, or
194/// a path that the renderer resolves on first use.
195#[non_exhaustive]
196#[derive(Clone, Debug)]
197pub enum ImageSource {
198    /// Fully-decoded frame, `Arc`-shared so cloning is cheap.
199    Decoded(Arc<oxideav_core::VideoFrame>),
200    /// Filesystem path — resolved lazily by the renderer.
201    Path(String),
202    /// Raw bytes of an encoded image file (PNG/JPEG/etc).
203    EncodedBytes(Arc<[u8]>),
204}
205
206/// Video source. Resolves packets via the container layer on
207/// demand; the scene renderer advances it to the requested PTS.
208#[non_exhaustive]
209#[derive(Clone, Debug)]
210pub enum VideoSource {
211    Path(String),
212    EncodedBytes(Arc<[u8]>),
213}
214
215/// Styled text run. Font resolution + shaping land in a separate
216/// crate; this type only carries what the model needs to preserve
217/// (the string itself + structural + appearance metadata).
218#[derive(Clone, Debug, Default)]
219pub struct TextRun {
220    pub text: String,
221    pub font_family: String,
222    pub font_weight: u16,
223    pub font_size: f32,
224    /// `0xRRGGBBAA`.
225    pub color: u32,
226    /// Optional explicit glyph-advance vector (PDF-style). If
227    /// `None`, the rasteriser shapes on the fly.
228    pub advances: Option<Vec<f32>>,
229    pub italic: bool,
230    pub underline: bool,
231}
232
233/// Vector shape primitive.
234#[non_exhaustive]
235#[derive(Clone, Debug)]
236pub enum Shape {
237    Rect {
238        width: f32,
239        height: f32,
240        fill: u32,
241        stroke: Option<Stroke>,
242        corner_radius: f32,
243    },
244    Polygon {
245        points: Vec<(f32, f32)>,
246        fill: u32,
247        stroke: Option<Stroke>,
248    },
249    Path {
250        /// SVG path data ("M10,10 L20,20 …").
251        data: String,
252        fill: u32,
253        stroke: Option<Stroke>,
254    },
255}
256
257impl Shape {
258    /// Zero-size placeholder rect with no fill. Used by
259    /// `SceneObject::default`.
260    pub const fn rect(width: f32, height: f32) -> Self {
261        Shape::Rect {
262            width,
263            height,
264            fill: 0,
265            stroke: None,
266            corner_radius: 0.0,
267        }
268    }
269}
270
271#[derive(Clone, Copy, Debug, PartialEq)]
272pub struct Stroke {
273    pub color: u32,
274    pub width: f32,
275}
276
277/// Opaque handle to a live input feed. The renderer polls it for
278/// the most recent frame at render time.
279#[derive(Clone, Debug)]
280pub struct LiveStreamHandle {
281    /// Implementation-defined URI — `rtmp://…`, `file://named-pipe`,
282    /// etc. The streaming compositor resolves this against a
283    /// pluggable `LiveSource` registry (pending crate).
284    pub uri: String,
285    /// Optional hint for the expected frame size. The renderer will
286    /// fall back to the actual frame size if it differs.
287    pub hint_size: Option<(u32, u32)>,
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn raster_canvas_size() {
296        let c = Canvas::raster(640, 480);
297        assert_eq!(c.raster_size(), Some((640, 480)));
298    }
299
300    #[test]
301    fn vector_canvas_no_raster_size() {
302        let c = Canvas::Vector {
303            width: 595.0,
304            height: 842.0,
305            unit: LengthUnit::Point,
306        };
307        assert!(c.raster_size().is_none());
308    }
309
310    #[test]
311    fn transform_identity_roundtrip() {
312        let t = Transform::identity();
313        assert_eq!(t.position, (0.0, 0.0));
314        assert_eq!(t.scale, (1.0, 1.0));
315        assert_eq!(t.anchor, (0.5, 0.5));
316    }
317
318    #[test]
319    fn scene_object_default_is_neutral() {
320        let o = SceneObject::default();
321        assert_eq!(o.opacity, 1.0);
322        assert_eq!(o.blend_mode, BlendMode::Normal);
323        assert!(o.animations.is_empty());
324    }
325}