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}