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    },
142    /// The node tree of the current frame's scene.
143    Tree {
144        /// Nodes in tree order (parents before children).
145        nodes: Vec<NodeInfo>,
146    },
147    /// Current values of the selected node; sent after each frame while
148    /// something is selected.
149    Signals {
150        /// The selected node.
151        node_id: u32,
152        /// One entry per property.
153        values: Vec<SignalValue>,
154        /// Selection box as four corners `[x0, y0, x1, y1, x2, y2, x3, y3]`
155        /// in frame pixels (rotated nodes give a rotated box).
156        #[serde(default, skip_serializing_if = "Option::is_none")]
157        bounds: Option<[f64; 8]>,
158    },
159    /// Selection changed (answer to `Pick` and `Select`).
160    Selected {
161        /// Selected node, if any.
162        node_id: Option<u32>,
163    },
164    /// Playback state changed (also sent while playing).
165    Playback {
166        /// Whether playback is running.
167        playing: bool,
168        /// Current global frame.
169        frame: u32,
170    },
171    /// A scene's meta changed and was saved.
172    MetaChanged {
173        /// Scene name.
174        scene: String,
175        /// New contents.
176        meta: Meta,
177    },
178}
179
180/// Category of an [`Event::Error`].
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
183pub enum ErrorKind {
184    /// Protocol versions of the UI and engine differ.
185    Incompatible,
186    /// The project does not compile.
187    Build,
188    /// The scene panicked or failed.
189    Runtime,
190    /// A `meta.json` file is invalid.
191    Meta,
192}
193
194/// A source location in an error.
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
197pub struct Span {
198    /// Path relative to the project.
199    pub file: String,
200    /// 1-based line.
201    pub line: u32,
202    /// 1-based column.
203    pub column: u32,
204    #[serde(default)]
205    /// Label for this location, may be empty.
206    pub message: String,
207}
208
209/// A scene on the timeline.
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
212pub struct SceneInfo {
213    /// Scene name (its function name).
214    pub name: String,
215    /// First global frame of the scene.
216    pub start: u32,
217    /// Length in frames.
218    pub frames: u32,
219    #[serde(default)]
220    /// Incoming blend length in frames. 0 if this scene has no transition.
221    pub transition: u32,
222    #[serde(default)]
223    /// Frame inside the scene where that blend starts.
224    pub transition_at: u32,
225}
226
227/// A sound effect on the timeline.
228#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
230pub struct AudioClipInfo {
231    /// File name.
232    pub name: String,
233    /// Start on the project timeline, in seconds.
234    pub start: f64,
235    /// Length in seconds. `0` when the file was not measured.
236    pub seconds: f64,
237    /// This clip ducks the soundtrack.
238    pub priority: bool,
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    /// Spoken text, when this clip is a line.
241    pub text: Option<String>,
242}
243
244/// A marker on the timeline.
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
247pub struct MarkerInfo {
248    /// Scene the marker belongs to.
249    pub scene: String,
250    /// Marker name.
251    pub name: String,
252    /// Seconds from the start of its scene.
253    pub time: f64,
254}
255
256/// A node in [`Event::Tree`].
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
259pub struct NodeInfo {
260    /// Node index.
261    pub id: u32,
262    /// Parent index; `None` for the root.
263    pub parent: Option<u32>,
264    /// Node type, e.g. `"Rect"`.
265    pub kind: String,
266    /// Name set with `.name(..)`.
267    pub name: Option<String>,
268}
269
270/// A property value in [`Event::Signals`].
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
272#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
273pub struct SignalValue {
274    /// Property name.
275    pub name: String,
276    /// Human-readable value, e.g. `"120.5"` or `"#ff0000ff"`.
277    pub value: String,
278}
279
280/// Image encoding of a binary preview frame.
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282#[repr(u8)]
283pub enum FrameFormat {
284    /// WebP image.
285    Webp = 1,
286    /// JPEG image.
287    Jpeg = 2,
288    /// Raw RGBA8, `width * height * 4` bytes.
289    Rgba = 3,
290    /// No body: the frame was drawn directly into the transport's surface
291    /// (an OffscreenCanvas in the browser).
292    Surface = 4,
293    /// Signed 16-bit little-endian mono PCM. `width` is the sample rate,
294    /// `height` is 1, and the body is the samples.
295    Pcm = 5,
296}
297
298impl FrameFormat {
299    /// Parses the header byte; `None` for unknown values.
300    pub fn from_u8(v: u8) -> Option<Self> {
301        match v {
302            1 => Some(Self::Webp),
303            2 => Some(Self::Jpeg),
304            3 => Some(Self::Rgba),
305            4 => Some(Self::Surface),
306            5 => Some(Self::Pcm),
307            _ => None,
308        }
309    }
310}
311
312/// 16-byte little-endian header of a binary WS frame:
313///
314/// | bytes  | field    |
315/// |--------|----------|
316/// | 0..4   | `frame`  |
317/// | 4..6   | `width`  |
318/// | 6..8   | `height` |
319/// | 8      | `format` |
320/// | 9..12  | reserved, zero |
321/// | 12..16 | `len` of the body that follows |
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub struct FrameHeader {
324    /// Global frame index.
325    pub frame: u32,
326    /// Image width in pixels.
327    pub width: u16,
328    /// Image height in pixels.
329    pub height: u16,
330    /// Encoding of the body.
331    pub format: FrameFormat,
332    /// Body length in bytes.
333    pub len: u32,
334}
335
336impl FrameHeader {
337    /// Header length in bytes.
338    pub const SIZE: usize = 16;
339
340    /// Serializes the header.
341    pub fn encode(&self) -> [u8; Self::SIZE] {
342        let mut out = [0u8; Self::SIZE];
343        out[0..4].copy_from_slice(&self.frame.to_le_bytes());
344        out[4..6].copy_from_slice(&self.width.to_le_bytes());
345        out[6..8].copy_from_slice(&self.height.to_le_bytes());
346        out[8] = self.format as u8;
347        out[12..16].copy_from_slice(&self.len.to_le_bytes());
348        out
349    }
350
351    /// Parses a header from the start of `bytes`; `None` if too short or unknown format.
352    pub fn decode(bytes: &[u8]) -> Option<Self> {
353        let b: &[u8; Self::SIZE] = bytes.get(..Self::SIZE)?.try_into().ok()?;
354        Some(Self {
355            frame: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
356            width: u16::from_le_bytes([b[4], b[5]]),
357            height: u16::from_le_bytes([b[6], b[7]]),
358            format: FrameFormat::from_u8(b[8])?,
359            len: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
360        })
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn command_json_shape() {
370        let json = serde_json::to_string(&Command::Seek { frame: 42 }).unwrap();
371        assert_eq!(json, r#"{"type":"Seek","frame":42}"#);
372        let back: Command = serde_json::from_str(r#"{"type":"Play"}"#).unwrap();
373        assert_eq!(back, Command::Play);
374    }
375
376    #[test]
377    fn frame_header_roundtrip() {
378        let h = FrameHeader {
379            frame: 1234,
380            width: 960,
381            height: 540,
382            format: FrameFormat::Webp,
383            len: 65_000,
384        };
385        assert_eq!(FrameHeader::decode(&h.encode()), Some(h));
386    }
387}