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