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