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    /// Hit-test at a point in frame pixels of the project size (origin
67    /// top-left). Answered with `Selected`.
68    Pick {
69        /// Horizontal position.
70        x: f64,
71        /// Vertical position.
72        y: f64,
73    },
74    /// Selects a node in the inspector, or clears the selection.
75    Select {
76        /// Node to select; `None` clears.
77        node_id: Option<u32>,
78    },
79    /// Asks the engine to exit (native preview server).
80    Shutdown,
81}
82
83/// Engine → UI.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
86#[serde(tag = "type")]
87pub enum Event {
88    /// Answer to `Hello`: the engine and the project.
89    Ready {
90        /// [`PROTOCOL_VERSION`](crate::PROTOCOL_VERSION) of the engine.
91        protocol_version: u32,
92        /// Version of the oxframe crate.
93        engine_version: String,
94        /// Frames per second.
95        fps: u32,
96        /// Project size in pixels, `[width, height]`.
97        size: [u32; 2],
98        /// Scenes in order.
99        scenes: Vec<SceneInfo>,
100    },
101    /// A new build is running (sent after a hot reload).
102    Reloaded,
103    /// The CLI started rebuilding the project.
104    Building,
105    /// Something failed; the last good state stays visible.
106    Error {
107        /// What failed.
108        kind: ErrorKind,
109        /// Human-readable message.
110        message: String,
111        #[serde(default)]
112        /// Source locations, for compile errors.
113        spans: Vec<Span>,
114    },
115    /// Followed by a binary frame (see [`FrameHeader`]).
116    Frame {
117        /// Global frame index.
118        index: u32,
119    },
120    /// Timeline layout; sent on start and when it changes.
121    Timeline {
122        /// Total number of frames.
123        frames: u32,
124        /// All markers of all scenes.
125        markers: Vec<MarkerInfo>,
126        /// Scenes in order.
127        scenes: Vec<SceneInfo>,
128        #[serde(default, skip_serializing_if = "Option::is_none")]
129        /// Peak amplitudes of the soundtrack in `0..=1`, one per frame (at most 4096).
130        audio_waveform: Option<Vec<f32>>,
131    },
132    /// The node tree of the current frame's scene.
133    Tree {
134        /// Nodes in tree order (parents before children).
135        nodes: Vec<NodeInfo>,
136    },
137    /// Current values of the selected node; sent after each frame while
138    /// something is selected.
139    Signals {
140        /// The selected node.
141        node_id: u32,
142        /// One entry per property.
143        values: Vec<SignalValue>,
144        /// Selection box as four corners `[x0, y0, x1, y1, x2, y2, x3, y3]`
145        /// in frame pixels (rotated nodes give a rotated box).
146        #[serde(default, skip_serializing_if = "Option::is_none")]
147        bounds: Option<[f64; 8]>,
148    },
149    /// Selection changed (answer to `Pick` and `Select`).
150    Selected {
151        /// Selected node, if any.
152        node_id: Option<u32>,
153    },
154    /// Playback state changed (also sent while playing).
155    Playback {
156        /// Whether playback is running.
157        playing: bool,
158        /// Current global frame.
159        frame: u32,
160    },
161    /// A scene's meta changed and was saved.
162    MetaChanged {
163        /// Scene name.
164        scene: String,
165        /// New contents.
166        meta: Meta,
167    },
168}
169
170/// Category of an [`Event::Error`].
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
172#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
173pub enum ErrorKind {
174    /// Protocol versions of the UI and engine differ.
175    Incompatible,
176    /// The project does not compile.
177    Build,
178    /// The scene panicked or failed.
179    Runtime,
180    /// A `meta.json` file is invalid.
181    Meta,
182}
183
184/// A source location in an error.
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
187pub struct Span {
188    /// Path relative to the project.
189    pub file: String,
190    /// 1-based line.
191    pub line: u32,
192    /// 1-based column.
193    pub column: u32,
194    #[serde(default)]
195    /// Label for this location, may be empty.
196    pub message: String,
197}
198
199/// A scene on the timeline.
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
202pub struct SceneInfo {
203    /// Scene name (its function name).
204    pub name: String,
205    /// First global frame of the scene.
206    pub start: u32,
207    /// Length in frames.
208    pub frames: u32,
209}
210
211/// A marker on the timeline.
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
214pub struct MarkerInfo {
215    /// Scene the marker belongs to.
216    pub scene: String,
217    /// Marker name.
218    pub name: String,
219    /// Seconds from the start of its scene.
220    pub time: f64,
221}
222
223/// A node in [`Event::Tree`].
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
226pub struct NodeInfo {
227    /// Node index.
228    pub id: u32,
229    /// Parent index; `None` for the root.
230    pub parent: Option<u32>,
231    /// Node type, e.g. `"Rect"`.
232    pub kind: String,
233    /// Name set with `.name(..)`.
234    pub name: Option<String>,
235}
236
237/// A property value in [`Event::Signals`].
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
240pub struct SignalValue {
241    /// Property name.
242    pub name: String,
243    /// Human-readable value, e.g. `"120.5"` or `"#ff0000ff"`.
244    pub value: String,
245}
246
247/// Image encoding of a binary preview frame.
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249#[repr(u8)]
250pub enum FrameFormat {
251    /// WebP image.
252    Webp = 1,
253    /// JPEG image.
254    Jpeg = 2,
255    /// Raw RGBA8, `width * height * 4` bytes.
256    Rgba = 3,
257    /// No body: the frame was drawn directly into the transport's surface
258    /// (an OffscreenCanvas in the browser).
259    Surface = 4,
260}
261
262impl FrameFormat {
263    /// Parses the header byte; `None` for unknown values.
264    pub fn from_u8(v: u8) -> Option<Self> {
265        match v {
266            1 => Some(Self::Webp),
267            2 => Some(Self::Jpeg),
268            3 => Some(Self::Rgba),
269            4 => Some(Self::Surface),
270            _ => None,
271        }
272    }
273}
274
275/// 16-byte little-endian header of a binary WS frame:
276///
277/// | bytes  | field    |
278/// |--------|----------|
279/// | 0..4   | `frame`  |
280/// | 4..6   | `width`  |
281/// | 6..8   | `height` |
282/// | 8      | `format` |
283/// | 9..12  | reserved, zero |
284/// | 12..16 | `len` of the body that follows |
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub struct FrameHeader {
287    /// Global frame index.
288    pub frame: u32,
289    /// Image width in pixels.
290    pub width: u16,
291    /// Image height in pixels.
292    pub height: u16,
293    /// Encoding of the body.
294    pub format: FrameFormat,
295    /// Body length in bytes.
296    pub len: u32,
297}
298
299impl FrameHeader {
300    /// Header length in bytes.
301    pub const SIZE: usize = 16;
302
303    /// Serializes the header.
304    pub fn encode(&self) -> [u8; Self::SIZE] {
305        let mut out = [0u8; Self::SIZE];
306        out[0..4].copy_from_slice(&self.frame.to_le_bytes());
307        out[4..6].copy_from_slice(&self.width.to_le_bytes());
308        out[6..8].copy_from_slice(&self.height.to_le_bytes());
309        out[8] = self.format as u8;
310        out[12..16].copy_from_slice(&self.len.to_le_bytes());
311        out
312    }
313
314    /// Parses a header from the start of `bytes`; `None` if too short or unknown format.
315    pub fn decode(bytes: &[u8]) -> Option<Self> {
316        let b: &[u8; Self::SIZE] = bytes.get(..Self::SIZE)?.try_into().ok()?;
317        Some(Self {
318            frame: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
319            width: u16::from_le_bytes([b[4], b[5]]),
320            height: u16::from_le_bytes([b[6], b[7]]),
321            format: FrameFormat::from_u8(b[8])?,
322            len: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
323        })
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn command_json_shape() {
333        let json = serde_json::to_string(&Command::Seek { frame: 42 }).unwrap();
334        assert_eq!(json, r#"{"type":"Seek","frame":42}"#);
335        let back: Command = serde_json::from_str(r#"{"type":"Play"}"#).unwrap();
336        assert_eq!(back, Command::Play);
337    }
338
339    #[test]
340    fn frame_header_roundtrip() {
341        let h = FrameHeader {
342            frame: 1234,
343            width: 960,
344            height: 540,
345            format: FrameFormat::Webp,
346            len: 65_000,
347        };
348        assert_eq!(FrameHeader::decode(&h.encode()), Some(h));
349    }
350}