Skip to main content

oxframe_protocol/
messages.rs

1use serde::{Deserialize, Serialize};
2
3use crate::Meta;
4
5/// UI → engine.
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
8#[serde(tag = "type")]
9pub enum Command {
10    /// First message after connecting; answered with `Ready`.
11    Hello {
12        /// [`PROTOCOL_VERSION`](crate::PROTOCOL_VERSION) of the UI.
13        protocol_version: u32,
14        /// Version of the editor build, for error messages.
15        ui_version: String,
16    },
17    /// Show this frame.
18    Seek {
19        /// Global frame index.
20        frame: u32,
21    },
22    /// Start playback from the current frame.
23    Play,
24    /// Stop playback.
25    Pause,
26    /// Move by a number of frames.
27    Step {
28        /// Frames to move; negative goes back.
29        delta: i32,
30    },
31    /// Size the preview is shown at; frames are rendered at `w·dpr × h·dpr`,
32    /// capped at the project size.
33    SetViewport {
34        /// Width in CSS pixels (including the editor's zoom).
35        w: u32,
36        /// Height in CSS pixels (including the editor's zoom).
37        h: u32,
38        /// Device pixel ratio.
39        dpr: f64,
40    },
41    /// Moves a marker (seconds from the start of its scene). With `scene`,
42    /// creates the marker there if it does not exist yet.
43    SetMarker {
44        /// Marker name.
45        name: String,
46        /// Seconds from the start of the scene.
47        time: f64,
48        #[serde(default, skip_serializing_if = "Option::is_none")]
49        /// Scene to create the marker in.
50        scene: Option<String>,
51    },
52    /// Changes a named ease.
53    SetEase {
54        /// Ease name.
55        name: String,
56        /// Control points `[x1, y1, x2, y2]`.
57        value: [f64; 4],
58    },
59    /// Changes a tunable.
60    SetTunable {
61        /// Tunable name.
62        name: String,
63        /// New value.
64        value: f64,
65    },
66    /// Sets how long the transition into `scene` lasts, in seconds.
67    SetTransition {
68        /// Scene the transition belongs to.
69        scene: String,
70        /// Blend length. Zero removes it.
71        seconds: f64,
72    },
73    /// Hit-test at a point in frame pixels of the project size (origin
74    /// top-left). Answered with `Selected`.
75    Pick {
76        /// Horizontal position.
77        x: f64,
78        /// Vertical position.
79        y: f64,
80    },
81    /// Selects a node in the inspector, or clears the selection.
82    Select {
83        /// Node to select; `None` clears.
84        node_id: Option<u32>,
85    },
86    /// Asks the engine to exit (native preview server).
87    Shutdown,
88}
89
90/// Engine → UI.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
93#[serde(tag = "type")]
94pub enum Event {
95    /// Answer to `Hello`: the engine and the project.
96    Ready {
97        /// [`PROTOCOL_VERSION`](crate::PROTOCOL_VERSION) of the engine.
98        protocol_version: u32,
99        /// Version of the oxframe crate.
100        engine_version: String,
101        /// Frames per second.
102        fps: u32,
103        /// Project size in pixels, `[width, height]`.
104        size: [u32; 2],
105        /// Scenes in order.
106        scenes: Vec<SceneInfo>,
107    },
108    /// A new build is running (sent after a hot reload).
109    Reloaded,
110    /// The CLI started rebuilding the project.
111    Building,
112    /// Something failed; the last good state stays visible.
113    Error {
114        /// What failed.
115        kind: ErrorKind,
116        /// Human-readable message.
117        message: String,
118        #[serde(default)]
119        /// Source locations, for compile errors.
120        spans: Vec<Span>,
121    },
122    /// Followed by a binary frame (see [`FrameHeader`]).
123    Frame {
124        /// Global frame index.
125        index: u32,
126    },
127    /// Timeline layout; sent on start and when it changes.
128    Timeline {
129        /// Total number of frames.
130        frames: u32,
131        /// All markers of all scenes.
132        markers: Vec<MarkerInfo>,
133        /// Scenes in order.
134        scenes: Vec<SceneInfo>,
135        #[serde(default, skip_serializing_if = "Option::is_none")]
136        /// Peak amplitudes of the soundtrack in `0..=1`, one per frame (at most 4096).
137        audio_waveform: Option<Vec<f32>>,
138        #[serde(default, skip_serializing_if = "Vec::is_empty")]
139        /// Sound effects on the timeline.
140        audio_clips: Vec<AudioClipInfo>,
141        #[serde(default, skip_serializing_if = "Vec::is_empty")]
142        /// Stops for `oxframe present`, in order: every `slide_break` and
143        /// the end of every scene.
144        slides: Vec<SlideInfo>,
145    },
146    /// The node tree of the current frame's scene.
147    Tree {
148        /// Nodes in tree order (parents before children).
149        nodes: Vec<NodeInfo>,
150    },
151    /// Current values of the selected node; sent after each frame while
152    /// something is selected.
153    Signals {
154        /// The selected node.
155        node_id: u32,
156        /// One entry per property.
157        values: Vec<SignalValue>,
158        /// Selection box as four corners `[x0, y0, x1, y1, x2, y2, x3, y3]`
159        /// in frame pixels (rotated nodes give a rotated box).
160        #[serde(default, skip_serializing_if = "Option::is_none")]
161        bounds: Option<[f64; 8]>,
162    },
163    /// Selection changed (answer to `Pick` and `Select`).
164    Selected {
165        /// Selected node, if any.
166        node_id: Option<u32>,
167    },
168    /// Playback state changed (also sent while playing).
169    Playback {
170        /// Whether playback is running.
171        playing: bool,
172        /// Current global frame.
173        frame: u32,
174    },
175    /// A scene's meta changed and was saved.
176    MetaChanged {
177        /// Scene name.
178        scene: String,
179        /// New contents.
180        meta: Meta,
181    },
182}
183
184/// Category of an [`Event::Error`].
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
187pub enum ErrorKind {
188    /// Protocol versions of the UI and engine differ.
189    Incompatible,
190    /// The project does not compile.
191    Build,
192    /// The scene panicked or failed.
193    Runtime,
194    /// A `meta.json` file is invalid.
195    Meta,
196}
197
198/// A source location in an error.
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
201pub struct Span {
202    /// Path relative to the project.
203    pub file: String,
204    /// 1-based line.
205    pub line: u32,
206    /// 1-based column.
207    pub column: u32,
208    #[serde(default)]
209    /// Label for this location, may be empty.
210    pub message: String,
211}
212
213/// A scene on the timeline.
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
216pub struct SceneInfo {
217    /// Scene name (its function name).
218    pub name: String,
219    /// First global frame of the scene.
220    pub start: u32,
221    /// Length in frames.
222    pub frames: u32,
223    #[serde(default)]
224    /// Incoming blend length in frames. 0 if this scene has no transition.
225    pub transition: u32,
226    #[serde(default)]
227    /// Frame inside the scene where that blend starts.
228    pub transition_at: u32,
229}
230
231/// A sound effect on the timeline.
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
234pub struct AudioClipInfo {
235    /// File name.
236    pub name: String,
237    /// Start on the project timeline, in seconds.
238    pub start: f64,
239    /// Length in seconds. `0` when the file was not measured.
240    pub seconds: f64,
241    /// This clip ducks the soundtrack.
242    pub priority: bool,
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    /// Spoken text, when this clip is a line.
245    pub text: Option<String>,
246}
247
248/// A stop of the presentation (`oxframe present`).
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
251pub struct SlideInfo {
252    /// Scene the stop belongs to.
253    pub scene: String,
254    /// Global frame shown while the presentation waits here.
255    pub frame: u32,
256    #[serde(default)]
257    /// Speaker notes for the part that ends at this stop; may be empty.
258    pub notes: String,
259}
260
261/// A marker on the timeline.
262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
263#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
264pub struct MarkerInfo {
265    /// Scene the marker belongs to.
266    pub scene: String,
267    /// Marker name.
268    pub name: String,
269    /// Seconds from the start of its scene.
270    pub time: f64,
271}
272
273/// A node in [`Event::Tree`].
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
276pub struct NodeInfo {
277    /// Node index.
278    pub id: u32,
279    /// Parent index; `None` for the root.
280    pub parent: Option<u32>,
281    /// Node type, e.g. `"Rect"`.
282    pub kind: String,
283    /// Name set with `.name(..)`.
284    pub name: Option<String>,
285}
286
287/// A property value in [`Event::Signals`].
288#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
289#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
290pub struct SignalValue {
291    /// Property name.
292    pub name: String,
293    /// Human-readable value, e.g. `"120.5"` or `"#ff0000ff"`.
294    pub value: String,
295}
296
297/// Image encoding of a binary preview frame.
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299#[repr(u8)]
300pub enum FrameFormat {
301    /// WebP image.
302    Webp = 1,
303    /// JPEG image.
304    Jpeg = 2,
305    /// Raw RGBA8, `width * height * 4` bytes.
306    Rgba = 3,
307    /// No body: the frame was drawn directly into the transport's surface
308    /// (an OffscreenCanvas in the browser).
309    Surface = 4,
310    /// Signed 16-bit little-endian mono PCM. `width` is the sample rate,
311    /// `height` is 1, and the body is the samples.
312    Pcm = 5,
313}
314
315impl FrameFormat {
316    /// Parses the header byte; `None` for unknown values.
317    pub fn from_u8(v: u8) -> Option<Self> {
318        match v {
319            1 => Some(Self::Webp),
320            2 => Some(Self::Jpeg),
321            3 => Some(Self::Rgba),
322            4 => Some(Self::Surface),
323            5 => Some(Self::Pcm),
324            _ => None,
325        }
326    }
327}
328
329/// 16-byte little-endian header of a binary WS frame:
330///
331/// | bytes  | field    |
332/// |--------|----------|
333/// | 0..4   | `frame`  |
334/// | 4..6   | `width`  |
335/// | 6..8   | `height` |
336/// | 8      | `format` |
337/// | 9..12  | reserved, zero |
338/// | 12..16 | `len` of the body that follows |
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub struct FrameHeader {
341    /// Global frame index.
342    pub frame: u32,
343    /// Image width in pixels.
344    pub width: u16,
345    /// Image height in pixels.
346    pub height: u16,
347    /// Encoding of the body.
348    pub format: FrameFormat,
349    /// Body length in bytes.
350    pub len: u32,
351}
352
353impl FrameHeader {
354    /// Header length in bytes.
355    pub const SIZE: usize = 16;
356
357    /// Serializes the header.
358    pub fn encode(&self) -> [u8; Self::SIZE] {
359        let mut out = [0u8; Self::SIZE];
360        out[0..4].copy_from_slice(&self.frame.to_le_bytes());
361        out[4..6].copy_from_slice(&self.width.to_le_bytes());
362        out[6..8].copy_from_slice(&self.height.to_le_bytes());
363        out[8] = self.format as u8;
364        out[12..16].copy_from_slice(&self.len.to_le_bytes());
365        out
366    }
367
368    /// Parses a header from the start of `bytes`; `None` if too short or unknown format.
369    pub fn decode(bytes: &[u8]) -> Option<Self> {
370        let b: &[u8; Self::SIZE] = bytes.get(..Self::SIZE)?.try_into().ok()?;
371        Some(Self {
372            frame: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
373            width: u16::from_le_bytes([b[4], b[5]]),
374            height: u16::from_le_bytes([b[6], b[7]]),
375            format: FrameFormat::from_u8(b[8])?,
376            len: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
377        })
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn command_json_shape() {
387        let json = serde_json::to_string(&Command::Seek { frame: 42 }).unwrap();
388        assert_eq!(json, r#"{"type":"Seek","frame":42}"#);
389        let back: Command = serde_json::from_str(r#"{"type":"Play"}"#).unwrap();
390        assert_eq!(back, Command::Play);
391    }
392
393    #[test]
394    fn frame_header_roundtrip() {
395        let h = FrameHeader {
396            frame: 1234,
397            width: 960,
398            height: 540,
399            format: FrameFormat::Webp,
400            len: 65_000,
401        };
402        assert_eq!(FrameHeader::decode(&h.encode()), Some(h));
403    }
404}