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}