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}
240
241/// A marker on the timeline.
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
244pub struct MarkerInfo {
245 /// Scene the marker belongs to.
246 pub scene: String,
247 /// Marker name.
248 pub name: String,
249 /// Seconds from the start of its scene.
250 pub time: f64,
251}
252
253/// A node in [`Event::Tree`].
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
256pub struct NodeInfo {
257 /// Node index.
258 pub id: u32,
259 /// Parent index; `None` for the root.
260 pub parent: Option<u32>,
261 /// Node type, e.g. `"Rect"`.
262 pub kind: String,
263 /// Name set with `.name(..)`.
264 pub name: Option<String>,
265}
266
267/// A property value in [`Event::Signals`].
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))]
270pub struct SignalValue {
271 /// Property name.
272 pub name: String,
273 /// Human-readable value, e.g. `"120.5"` or `"#ff0000ff"`.
274 pub value: String,
275}
276
277/// Image encoding of a binary preview frame.
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279#[repr(u8)]
280pub enum FrameFormat {
281 /// WebP image.
282 Webp = 1,
283 /// JPEG image.
284 Jpeg = 2,
285 /// Raw RGBA8, `width * height * 4` bytes.
286 Rgba = 3,
287 /// No body: the frame was drawn directly into the transport's surface
288 /// (an OffscreenCanvas in the browser).
289 Surface = 4,
290 /// Signed 16-bit little-endian mono PCM. `width` is the sample rate,
291 /// `height` is 1, and the body is the samples.
292 Pcm = 5,
293}
294
295impl FrameFormat {
296 /// Parses the header byte; `None` for unknown values.
297 pub fn from_u8(v: u8) -> Option<Self> {
298 match v {
299 1 => Some(Self::Webp),
300 2 => Some(Self::Jpeg),
301 3 => Some(Self::Rgba),
302 4 => Some(Self::Surface),
303 5 => Some(Self::Pcm),
304 _ => None,
305 }
306 }
307}
308
309/// 16-byte little-endian header of a binary WS frame:
310///
311/// | bytes | field |
312/// |--------|----------|
313/// | 0..4 | `frame` |
314/// | 4..6 | `width` |
315/// | 6..8 | `height` |
316/// | 8 | `format` |
317/// | 9..12 | reserved, zero |
318/// | 12..16 | `len` of the body that follows |
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub struct FrameHeader {
321 /// Global frame index.
322 pub frame: u32,
323 /// Image width in pixels.
324 pub width: u16,
325 /// Image height in pixels.
326 pub height: u16,
327 /// Encoding of the body.
328 pub format: FrameFormat,
329 /// Body length in bytes.
330 pub len: u32,
331}
332
333impl FrameHeader {
334 /// Header length in bytes.
335 pub const SIZE: usize = 16;
336
337 /// Serializes the header.
338 pub fn encode(&self) -> [u8; Self::SIZE] {
339 let mut out = [0u8; Self::SIZE];
340 out[0..4].copy_from_slice(&self.frame.to_le_bytes());
341 out[4..6].copy_from_slice(&self.width.to_le_bytes());
342 out[6..8].copy_from_slice(&self.height.to_le_bytes());
343 out[8] = self.format as u8;
344 out[12..16].copy_from_slice(&self.len.to_le_bytes());
345 out
346 }
347
348 /// Parses a header from the start of `bytes`; `None` if too short or unknown format.
349 pub fn decode(bytes: &[u8]) -> Option<Self> {
350 let b: &[u8; Self::SIZE] = bytes.get(..Self::SIZE)?.try_into().ok()?;
351 Some(Self {
352 frame: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
353 width: u16::from_le_bytes([b[4], b[5]]),
354 height: u16::from_le_bytes([b[6], b[7]]),
355 format: FrameFormat::from_u8(b[8])?,
356 len: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
357 })
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn command_json_shape() {
367 let json = serde_json::to_string(&Command::Seek { frame: 42 }).unwrap();
368 assert_eq!(json, r#"{"type":"Seek","frame":42}"#);
369 let back: Command = serde_json::from_str(r#"{"type":"Play"}"#).unwrap();
370 assert_eq!(back, Command::Play);
371 }
372
373 #[test]
374 fn frame_header_roundtrip() {
375 let h = FrameHeader {
376 frame: 1234,
377 width: 960,
378 height: 540,
379 format: FrameFormat::Webp,
380 len: 65_000,
381 };
382 assert_eq!(FrameHeader::decode(&h.encode()), Some(h));
383 }
384}