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