Skip to main content

runmat_plot/
event.rs

1use crate::core::{BoundingBox, Vertex};
2use crate::plots::{
3    AreaPlot, AxesMetadata, BarChart, ColorMap, ContourFillPlot, ContourPlot, ErrorBar, Figure,
4    LegendEntry, LegendStyle, Line3Plot, LinePlot, MarkerStyle, PlotElement, PlotType, QuiverPlot,
5    Scatter3Plot, ScatterPlot, ShadingMode, StairsPlot, StemPlot, SurfacePlot, TextStyle,
6};
7use glam::{Vec3, Vec4};
8use serde::{Deserialize, Serialize};
9
10/// High-level event emitted whenever a figure changes.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct FigureEvent {
14    pub handle: u32,
15    pub kind: FigureEventKind,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub figure: Option<FigureSnapshot>,
18}
19
20/// Event kind for figure lifecycle + updates.
21#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(rename_all = "lowercase")]
23pub enum FigureEventKind {
24    Created,
25    Updated,
26    Cleared,
27    Closed,
28}
29
30/// Snapshot of the figure state describing layout + plots.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct FigureSnapshot {
34    pub layout: FigureLayout,
35    pub metadata: FigureMetadata,
36    pub plots: Vec<PlotDescriptor>,
37}
38
39/// Full replay scene payload capable of reconstructing a figure.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct FigureScene {
43    pub schema_version: u32,
44    pub layout: FigureLayout,
45    pub metadata: FigureMetadata,
46    pub plots: Vec<ScenePlot>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(tag = "kind", rename_all = "snake_case")]
51pub enum ScenePlot {
52    Line {
53        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
54        x: Vec<f64>,
55        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
56        y: Vec<f64>,
57        color_rgba: [f32; 4],
58        line_width: f32,
59        line_style: String,
60        axes_index: u32,
61        label: Option<String>,
62        visible: bool,
63    },
64    Scatter {
65        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
66        x: Vec<f64>,
67        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
68        y: Vec<f64>,
69        color_rgba: [f32; 4],
70        marker_size: f32,
71        marker_style: String,
72        axes_index: u32,
73        label: Option<String>,
74        visible: bool,
75    },
76    Bar {
77        labels: Vec<String>,
78        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
79        values: Vec<f64>,
80        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
81        histogram_bin_edges: Option<Vec<f64>>,
82        color_rgba: [f32; 4],
83        #[serde(default)]
84        outline_color_rgba: Option<[f32; 4]>,
85        bar_width: f32,
86        outline_width: f32,
87        orientation: String,
88        group_index: u32,
89        group_count: u32,
90        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
91        stack_offsets: Option<Vec<f64>>,
92        axes_index: u32,
93        label: Option<String>,
94        visible: bool,
95    },
96    ErrorBar {
97        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
98        x: Vec<f64>,
99        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
100        y: Vec<f64>,
101        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
102        err_low: Vec<f64>,
103        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
104        err_high: Vec<f64>,
105        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
106        x_err_low: Vec<f64>,
107        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
108        x_err_high: Vec<f64>,
109        orientation: String,
110        color_rgba: [f32; 4],
111        line_width: f32,
112        line_style: String,
113        cap_width: f32,
114        marker_style: Option<String>,
115        marker_size: Option<f32>,
116        marker_face_color: Option<[f32; 4]>,
117        marker_edge_color: Option<[f32; 4]>,
118        marker_filled: Option<bool>,
119        axes_index: u32,
120        label: Option<String>,
121        visible: bool,
122    },
123    Stairs {
124        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
125        x: Vec<f64>,
126        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
127        y: Vec<f64>,
128        color_rgba: [f32; 4],
129        line_width: f32,
130        axes_index: u32,
131        label: Option<String>,
132        visible: bool,
133    },
134    Stem {
135        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
136        x: Vec<f64>,
137        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
138        y: Vec<f64>,
139        #[serde(deserialize_with = "deserialize_f64_lossy")]
140        baseline: f64,
141        color_rgba: [f32; 4],
142        line_width: f32,
143        line_style: String,
144        baseline_color_rgba: [f32; 4],
145        baseline_visible: bool,
146        marker_color_rgba: [f32; 4],
147        marker_size: f32,
148        marker_filled: bool,
149        axes_index: u32,
150        label: Option<String>,
151        visible: bool,
152    },
153    Area {
154        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
155        x: Vec<f64>,
156        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
157        y: Vec<f64>,
158        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
159        lower_y: Option<Vec<f64>>,
160        #[serde(deserialize_with = "deserialize_f64_lossy")]
161        baseline: f64,
162        color_rgba: [f32; 4],
163        axes_index: u32,
164        label: Option<String>,
165        visible: bool,
166    },
167    Quiver {
168        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
169        x: Vec<f64>,
170        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
171        y: Vec<f64>,
172        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
173        u: Vec<f64>,
174        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
175        v: Vec<f64>,
176        color_rgba: [f32; 4],
177        line_width: f32,
178        scale: f32,
179        head_size: f32,
180        axes_index: u32,
181        label: Option<String>,
182        visible: bool,
183    },
184    Surface {
185        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
186        x: Vec<f64>,
187        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
188        y: Vec<f64>,
189        #[serde(deserialize_with = "deserialize_matrix_f64_lossy")]
190        z: Vec<Vec<f64>>,
191        colormap: String,
192        shading_mode: String,
193        wireframe: bool,
194        alpha: f32,
195        flatten_z: bool,
196        #[serde(default)]
197        image_mode: bool,
198        #[serde(default)]
199        color_grid_rgba: Option<Vec<Vec<[f32; 4]>>>,
200        #[serde(default, deserialize_with = "deserialize_option_pair_f64_lossy")]
201        color_limits: Option<[f64; 2]>,
202        axes_index: u32,
203        label: Option<String>,
204        visible: bool,
205    },
206    Line3 {
207        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
208        x: Vec<f64>,
209        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
210        y: Vec<f64>,
211        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
212        z: Vec<f64>,
213        color_rgba: [f32; 4],
214        line_width: f32,
215        line_style: String,
216        axes_index: u32,
217        label: Option<String>,
218        visible: bool,
219    },
220    Scatter3 {
221        #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
222        points: Vec<[f32; 3]>,
223        #[serde(default, deserialize_with = "deserialize_vec_rgba_f32_lossy")]
224        colors_rgba: Vec<[f32; 4]>,
225        point_size: f32,
226        #[serde(default, deserialize_with = "deserialize_option_vec_f32_lossy")]
227        point_sizes: Option<Vec<f32>>,
228        axes_index: u32,
229        label: Option<String>,
230        visible: bool,
231    },
232    Contour {
233        vertices: Vec<SerializedVertex>,
234        bounds_min: [f32; 3],
235        bounds_max: [f32; 3],
236        base_z: f32,
237        line_width: f32,
238        axes_index: u32,
239        label: Option<String>,
240        visible: bool,
241    },
242    ContourFill {
243        vertices: Vec<SerializedVertex>,
244        bounds_min: [f32; 3],
245        bounds_max: [f32; 3],
246        axes_index: u32,
247        label: Option<String>,
248        visible: bool,
249    },
250    Pie {
251        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
252        values: Vec<f64>,
253        colors_rgba: Vec<[f32; 4]>,
254        slice_labels: Vec<String>,
255        label_format: Option<String>,
256        explode: Vec<bool>,
257        axes_index: u32,
258        label: Option<String>,
259        visible: bool,
260    },
261    Unsupported {
262        plot_kind: PlotKind,
263        axes_index: u32,
264        label: Option<String>,
265        visible: bool,
266    },
267}
268
269impl FigureSnapshot {
270    /// Capture a snapshot from a [`Figure`] reference.
271    pub fn capture(figure: &Figure) -> Self {
272        let (rows, cols) = figure.axes_grid();
273        let layout = FigureLayout {
274            axes_rows: rows as u32,
275            axes_cols: cols as u32,
276            axes_indices: figure
277                .plot_axes_indices()
278                .iter()
279                .map(|idx| *idx as u32)
280                .collect(),
281        };
282
283        let metadata = FigureMetadata::from_figure(figure);
284
285        let plots = figure
286            .plots()
287            .enumerate()
288            .map(|(idx, plot)| PlotDescriptor::from_plot(plot, figure_axis_index(figure, idx)))
289            .collect();
290
291        Self {
292            layout,
293            metadata,
294            plots,
295        }
296    }
297}
298
299impl FigureScene {
300    pub const SCHEMA_VERSION: u32 = 1;
301
302    pub fn capture(figure: &Figure) -> Self {
303        let snapshot = FigureSnapshot::capture(figure);
304        let plots = figure
305            .plots()
306            .enumerate()
307            .map(|(idx, plot)| ScenePlot::from_plot(plot, figure_axis_index(figure, idx)))
308            .collect();
309
310        Self {
311            schema_version: Self::SCHEMA_VERSION,
312            layout: snapshot.layout,
313            metadata: snapshot.metadata,
314            plots,
315        }
316    }
317
318    pub fn into_figure(self) -> Result<Figure, String> {
319        if self.schema_version != Self::SCHEMA_VERSION {
320            return Err(format!(
321                "unsupported figure scene schema version {}",
322                self.schema_version
323            ));
324        }
325
326        let mut figure = Figure::new();
327        figure.set_subplot_grid(
328            self.layout.axes_rows as usize,
329            self.layout.axes_cols as usize,
330        );
331        figure.active_axes_index = self.metadata.active_axes_index as usize;
332        if let Some(axes_metadata) = self.metadata.axes_metadata.clone() {
333            figure.axes_metadata = axes_metadata.into_iter().map(AxesMetadata::from).collect();
334            figure.set_active_axes_index(figure.active_axes_index);
335        } else {
336            figure.title = self.metadata.title;
337            figure.x_label = self.metadata.x_label;
338            figure.y_label = self.metadata.y_label;
339            figure.legend_enabled = self.metadata.legend_enabled;
340        }
341        figure.sg_title = self.metadata.sg_title;
342        figure.sg_title_style = self
343            .metadata
344            .sg_title_style
345            .map(TextStyle::from)
346            .unwrap_or_default();
347        figure.grid_enabled = self.metadata.grid_enabled;
348        figure.z_limits = self.metadata.z_limits.map(|[lo, hi]| (lo, hi));
349        figure.colorbar_enabled = self.metadata.colorbar_enabled;
350        figure.axis_equal = self.metadata.axis_equal;
351        figure.background_color = rgba_to_vec4(self.metadata.background_rgba);
352
353        for plot in self.plots {
354            plot.apply_to_figure(&mut figure)?;
355        }
356
357        Ok(figure)
358    }
359}
360
361fn figure_axis_index(figure: &Figure, plot_index: usize) -> u32 {
362    figure
363        .plot_axes_indices()
364        .get(plot_index)
365        .copied()
366        .unwrap_or(0) as u32
367}
368
369/// Layout metadata describing subplot arrangement.
370#[derive(Debug, Clone, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct FigureLayout {
373    pub axes_rows: u32,
374    pub axes_cols: u32,
375    pub axes_indices: Vec<u32>,
376}
377
378/// Figure-level metadata (title, labels, theming).
379#[derive(Debug, Clone, Serialize, Deserialize)]
380#[serde(rename_all = "camelCase")]
381pub struct FigureMetadata {
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub title: Option<String>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub sg_title: Option<String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub sg_title_style: Option<SerializedTextStyle>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub x_label: Option<String>,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub y_label: Option<String>,
392    pub grid_enabled: bool,
393    pub legend_enabled: bool,
394    pub colorbar_enabled: bool,
395    pub axis_equal: bool,
396    pub background_rgba: [f32; 4],
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub colormap: Option<String>,
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub color_limits: Option<[f64; 2]>,
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub z_limits: Option<[f64; 2]>,
403    pub legend_entries: Vec<FigureLegendEntry>,
404    #[serde(default)]
405    pub active_axes_index: u32,
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub axes_metadata: Option<Vec<SerializedAxesMetadata>>,
408}
409
410impl FigureMetadata {
411    fn from_figure(figure: &Figure) -> Self {
412        let legend_entries = figure
413            .legend_entries()
414            .into_iter()
415            .map(FigureLegendEntry::from)
416            .collect();
417
418        Self {
419            title: figure.title.clone(),
420            sg_title: figure.sg_title.clone(),
421            sg_title_style: figure
422                .sg_title
423                .as_ref()
424                .map(|_| figure.sg_title_style.clone().into()),
425            x_label: figure.x_label.clone(),
426            y_label: figure.y_label.clone(),
427            grid_enabled: figure.grid_enabled,
428            legend_enabled: figure.legend_enabled,
429            colorbar_enabled: figure.colorbar_enabled,
430            axis_equal: figure.axis_equal,
431            background_rgba: vec4_to_rgba(figure.background_color),
432            colormap: Some(format!("{:?}", figure.colormap)),
433            color_limits: figure.color_limits.map(|(lo, hi)| [lo, hi]),
434            z_limits: figure.z_limits.map(|(lo, hi)| [lo, hi]),
435            legend_entries,
436            active_axes_index: figure.active_axes_index as u32,
437            axes_metadata: Some(
438                figure
439                    .axes_metadata
440                    .iter()
441                    .cloned()
442                    .map(SerializedAxesMetadata::from)
443                    .collect(),
444            ),
445        }
446    }
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
450#[serde(rename_all = "camelCase")]
451pub struct SerializedTextStyle {
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub color_rgba: Option<[f32; 4]>,
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub font_size: Option<f32>,
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub font_weight: Option<String>,
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub font_angle: Option<String>,
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub interpreter: Option<String>,
462    pub visible: bool,
463}
464
465impl From<TextStyle> for SerializedTextStyle {
466    fn from(value: TextStyle) -> Self {
467        Self {
468            color_rgba: value.color.map(vec4_to_rgba),
469            font_size: value.font_size,
470            font_weight: value.font_weight,
471            font_angle: value.font_angle,
472            interpreter: value.interpreter,
473            visible: value.visible,
474        }
475    }
476}
477
478impl From<SerializedTextStyle> for TextStyle {
479    fn from(value: SerializedTextStyle) -> Self {
480        Self {
481            color: value.color_rgba.map(rgba_to_vec4),
482            font_size: value.font_size,
483            font_weight: value.font_weight,
484            font_angle: value.font_angle,
485            interpreter: value.interpreter,
486            visible: value.visible,
487        }
488    }
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
492#[serde(rename_all = "camelCase")]
493pub struct SerializedLegendStyle {
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub location: Option<String>,
496    pub visible: bool,
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub font_size: Option<f32>,
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub font_weight: Option<String>,
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub font_angle: Option<String>,
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub interpreter: Option<String>,
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub box_visible: Option<bool>,
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub orientation: Option<String>,
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub text_color_rgba: Option<[f32; 4]>,
511}
512
513impl From<LegendStyle> for SerializedLegendStyle {
514    fn from(value: LegendStyle) -> Self {
515        Self {
516            location: value.location,
517            visible: value.visible,
518            font_size: value.font_size,
519            font_weight: value.font_weight,
520            font_angle: value.font_angle,
521            interpreter: value.interpreter,
522            box_visible: value.box_visible,
523            orientation: value.orientation,
524            text_color_rgba: value.text_color.map(vec4_to_rgba),
525        }
526    }
527}
528
529impl From<SerializedLegendStyle> for LegendStyle {
530    fn from(value: SerializedLegendStyle) -> Self {
531        Self {
532            location: value.location,
533            visible: value.visible,
534            font_size: value.font_size,
535            font_weight: value.font_weight,
536            font_angle: value.font_angle,
537            interpreter: value.interpreter,
538            box_visible: value.box_visible,
539            orientation: value.orientation,
540            text_color: value.text_color_rgba.map(rgba_to_vec4),
541        }
542    }
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
546#[serde(rename_all = "camelCase")]
547pub struct SerializedAxesMetadata {
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub title: Option<String>,
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub x_label: Option<String>,
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub y_label: Option<String>,
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub z_label: Option<String>,
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub x_limits: Option<[f64; 2]>,
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub y_limits: Option<[f64; 2]>,
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub z_limits: Option<[f64; 2]>,
562    #[serde(default)]
563    pub x_log: bool,
564    #[serde(default)]
565    pub y_log: bool,
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub view_azimuth_deg: Option<f32>,
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub view_elevation_deg: Option<f32>,
570    #[serde(default)]
571    pub grid_enabled: bool,
572    #[serde(default)]
573    pub box_enabled: bool,
574    #[serde(default)]
575    pub axis_equal: bool,
576    pub legend_enabled: bool,
577    #[serde(default)]
578    pub colorbar_enabled: bool,
579    pub colormap: String,
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub color_limits: Option<[f64; 2]>,
582    pub title_style: SerializedTextStyle,
583    pub x_label_style: SerializedTextStyle,
584    pub y_label_style: SerializedTextStyle,
585    pub z_label_style: SerializedTextStyle,
586    pub legend_style: SerializedLegendStyle,
587    #[serde(default, skip_serializing_if = "Vec::is_empty")]
588    pub world_text_annotations: Vec<SerializedTextAnnotation>,
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593pub struct SerializedTextAnnotation {
594    pub position: [f32; 3],
595    pub text: String,
596    pub style: SerializedTextStyle,
597}
598
599impl From<AxesMetadata> for SerializedAxesMetadata {
600    fn from(value: AxesMetadata) -> Self {
601        Self {
602            title: value.title,
603            x_label: value.x_label,
604            y_label: value.y_label,
605            z_label: value.z_label,
606            x_limits: value.x_limits.map(|(a, b)| [a, b]),
607            y_limits: value.y_limits.map(|(a, b)| [a, b]),
608            z_limits: value.z_limits.map(|(a, b)| [a, b]),
609            x_log: value.x_log,
610            y_log: value.y_log,
611            view_azimuth_deg: value.view_azimuth_deg,
612            view_elevation_deg: value.view_elevation_deg,
613            grid_enabled: value.grid_enabled,
614            box_enabled: value.box_enabled,
615            axis_equal: value.axis_equal,
616            legend_enabled: value.legend_enabled,
617            colorbar_enabled: value.colorbar_enabled,
618            colormap: format!("{:?}", value.colormap),
619            color_limits: value.color_limits.map(|(a, b)| [a, b]),
620            title_style: value.title_style.into(),
621            x_label_style: value.x_label_style.into(),
622            y_label_style: value.y_label_style.into(),
623            z_label_style: value.z_label_style.into(),
624            legend_style: value.legend_style.into(),
625            world_text_annotations: value
626                .world_text_annotations
627                .into_iter()
628                .map(Into::into)
629                .collect(),
630        }
631    }
632}
633
634impl From<SerializedAxesMetadata> for AxesMetadata {
635    fn from(value: SerializedAxesMetadata) -> Self {
636        Self {
637            title: value.title,
638            x_label: value.x_label,
639            y_label: value.y_label,
640            z_label: value.z_label,
641            x_limits: value.x_limits.map(|[a, b]| (a, b)),
642            y_limits: value.y_limits.map(|[a, b]| (a, b)),
643            z_limits: value.z_limits.map(|[a, b]| (a, b)),
644            x_log: value.x_log,
645            y_log: value.y_log,
646            view_azimuth_deg: value.view_azimuth_deg,
647            view_elevation_deg: value.view_elevation_deg,
648            grid_enabled: value.grid_enabled,
649            box_enabled: value.box_enabled,
650            axis_equal: value.axis_equal,
651            legend_enabled: value.legend_enabled,
652            colorbar_enabled: value.colorbar_enabled,
653            colormap: parse_colormap_name(&value.colormap),
654            color_limits: value.color_limits.map(|[a, b]| (a, b)),
655            title_style: value.title_style.into(),
656            x_label_style: value.x_label_style.into(),
657            y_label_style: value.y_label_style.into(),
658            z_label_style: value.z_label_style.into(),
659            legend_style: value.legend_style.into(),
660            world_text_annotations: value
661                .world_text_annotations
662                .into_iter()
663                .map(Into::into)
664                .collect(),
665        }
666    }
667}
668
669impl From<crate::plots::figure::TextAnnotation> for SerializedTextAnnotation {
670    fn from(value: crate::plots::figure::TextAnnotation) -> Self {
671        Self {
672            position: value.position.to_array(),
673            text: value.text,
674            style: value.style.into(),
675        }
676    }
677}
678
679impl From<SerializedTextAnnotation> for crate::plots::figure::TextAnnotation {
680    fn from(value: SerializedTextAnnotation) -> Self {
681        Self {
682            position: glam::Vec3::from_array(value.position),
683            text: value.text,
684            style: value.style.into(),
685        }
686    }
687}
688
689/// Descriptor for a single plot element within the figure.
690#[derive(Debug, Clone, Serialize, Deserialize)]
691#[serde(rename_all = "camelCase")]
692pub struct PlotDescriptor {
693    pub kind: PlotKind,
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub label: Option<String>,
696    pub axes_index: u32,
697    pub color_rgba: [f32; 4],
698    pub visible: bool,
699}
700
701impl PlotDescriptor {
702    fn from_plot(plot: &PlotElement, axes_index: u32) -> Self {
703        Self {
704            kind: PlotKind::from(plot.plot_type()),
705            label: plot.label(),
706            axes_index,
707            color_rgba: vec4_to_rgba(plot.color()),
708            visible: plot.is_visible(),
709        }
710    }
711}
712
713impl ScenePlot {
714    fn from_plot(plot: &PlotElement, axes_index: u32) -> Self {
715        match plot {
716            PlotElement::Line(line) => Self::Line {
717                x: line.x_data.clone(),
718                y: line.y_data.clone(),
719                color_rgba: vec4_to_rgba(line.color),
720                line_width: line.line_width,
721                line_style: format!("{:?}", line.line_style),
722                axes_index,
723                label: line.label.clone(),
724                visible: line.visible,
725            },
726            PlotElement::Scatter(scatter) => Self::Scatter {
727                x: scatter.x_data.clone(),
728                y: scatter.y_data.clone(),
729                color_rgba: vec4_to_rgba(scatter.color),
730                marker_size: scatter.marker_size,
731                marker_style: format!("{:?}", scatter.marker_style),
732                axes_index,
733                label: scatter.label.clone(),
734                visible: scatter.visible,
735            },
736            PlotElement::Bar(bar) => Self::Bar {
737                labels: bar.labels.clone(),
738                values: bar.values().unwrap_or(&[]).to_vec(),
739                histogram_bin_edges: bar.histogram_bin_edges().map(|edges| edges.to_vec()),
740                color_rgba: vec4_to_rgba(bar.color),
741                outline_color_rgba: bar.outline_color.map(vec4_to_rgba),
742                bar_width: bar.bar_width,
743                outline_width: bar.outline_width,
744                orientation: format!("{:?}", bar.orientation),
745                group_index: bar.group_index as u32,
746                group_count: bar.group_count as u32,
747                stack_offsets: bar.stack_offsets().map(|offsets| offsets.to_vec()),
748                axes_index,
749                label: bar.label.clone(),
750                visible: bar.visible,
751            },
752            PlotElement::ErrorBar(error) => Self::ErrorBar {
753                x: error.x.clone(),
754                y: error.y.clone(),
755                err_low: error.y_neg.clone(),
756                err_high: error.y_pos.clone(),
757                x_err_low: error.x_neg.clone(),
758                x_err_high: error.x_pos.clone(),
759                orientation: format!("{:?}", error.orientation),
760                color_rgba: vec4_to_rgba(error.color),
761                line_width: error.line_width,
762                line_style: format!("{:?}", error.line_style),
763                cap_width: error.cap_size,
764                marker_style: error.marker.as_ref().map(|m| format!("{:?}", m.kind)),
765                marker_size: error.marker.as_ref().map(|m| m.size),
766                marker_face_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.face_color)),
767                marker_edge_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.edge_color)),
768                marker_filled: error.marker.as_ref().map(|m| m.filled),
769                axes_index,
770                label: error.label.clone(),
771                visible: error.visible,
772            },
773            PlotElement::Stairs(stairs) => Self::Stairs {
774                x: stairs.x.clone(),
775                y: stairs.y.clone(),
776                color_rgba: vec4_to_rgba(stairs.color),
777                line_width: stairs.line_width,
778                axes_index,
779                label: stairs.label.clone(),
780                visible: stairs.visible,
781            },
782            PlotElement::Stem(stem) => Self::Stem {
783                x: stem.x.clone(),
784                y: stem.y.clone(),
785                baseline: stem.baseline,
786                color_rgba: vec4_to_rgba(stem.color),
787                line_width: stem.line_width,
788                line_style: format!("{:?}", stem.line_style),
789                baseline_color_rgba: vec4_to_rgba(stem.baseline_color),
790                baseline_visible: stem.baseline_visible,
791                marker_color_rgba: vec4_to_rgba(
792                    stem.marker
793                        .as_ref()
794                        .map(|m| m.face_color)
795                        .unwrap_or(stem.color),
796                ),
797                marker_size: stem.marker.as_ref().map(|m| m.size).unwrap_or(0.0),
798                marker_filled: stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
799                axes_index,
800                label: stem.label.clone(),
801                visible: stem.visible,
802            },
803            PlotElement::Area(area) => Self::Area {
804                x: area.x.clone(),
805                y: area.y.clone(),
806                lower_y: area.lower_y.clone(),
807                baseline: area.baseline,
808                color_rgba: vec4_to_rgba(area.color),
809                axes_index,
810                label: area.label.clone(),
811                visible: area.visible,
812            },
813            PlotElement::Quiver(quiver) => Self::Quiver {
814                x: quiver.x.clone(),
815                y: quiver.y.clone(),
816                u: quiver.u.clone(),
817                v: quiver.v.clone(),
818                color_rgba: vec4_to_rgba(quiver.color),
819                line_width: quiver.line_width,
820                scale: quiver.scale,
821                head_size: quiver.head_size,
822                axes_index,
823                label: quiver.label.clone(),
824                visible: quiver.visible,
825            },
826            PlotElement::Surface(surface) => Self::Surface {
827                x: surface.x_data.clone(),
828                y: surface.y_data.clone(),
829                z: surface.z_data.clone().unwrap_or_default(),
830                colormap: format!("{:?}", surface.colormap),
831                shading_mode: format!("{:?}", surface.shading_mode),
832                wireframe: surface.wireframe,
833                alpha: surface.alpha,
834                flatten_z: surface.flatten_z,
835                image_mode: surface.image_mode,
836                color_grid_rgba: surface.color_grid.as_ref().map(|grid| {
837                    grid.iter()
838                        .map(|row| row.iter().map(|color| vec4_to_rgba(*color)).collect())
839                        .collect()
840                }),
841                color_limits: surface.color_limits.map(|(lo, hi)| [lo, hi]),
842                axes_index,
843                label: surface.label.clone(),
844                visible: surface.visible,
845            },
846            PlotElement::Line3(line) => Self::Line3 {
847                x: line.x_data.clone(),
848                y: line.y_data.clone(),
849                z: line.z_data.clone(),
850                color_rgba: vec4_to_rgba(line.color),
851                line_width: line.line_width,
852                line_style: format!("{:?}", line.line_style),
853                axes_index,
854                label: line.label.clone(),
855                visible: line.visible,
856            },
857            PlotElement::Scatter3(scatter3) => Self::Scatter3 {
858                points: scatter3
859                    .points
860                    .iter()
861                    .map(|point| vec3_to_xyz(*point))
862                    .collect(),
863                colors_rgba: scatter3
864                    .colors
865                    .iter()
866                    .map(|color| vec4_to_rgba(*color))
867                    .collect(),
868                point_size: scatter3.point_size,
869                point_sizes: scatter3.point_sizes.clone(),
870                axes_index,
871                label: scatter3.label.clone(),
872                visible: scatter3.visible,
873            },
874            PlotElement::Contour(contour) => Self::Contour {
875                vertices: contour
876                    .cpu_vertices()
877                    .unwrap_or(&[])
878                    .iter()
879                    .cloned()
880                    .map(Into::into)
881                    .collect(),
882                bounds_min: vec3_to_xyz(contour.bounds().min),
883                bounds_max: vec3_to_xyz(contour.bounds().max),
884                base_z: contour.base_z,
885                line_width: contour.line_width,
886                axes_index,
887                label: contour.label.clone(),
888                visible: contour.visible,
889            },
890            PlotElement::ContourFill(fill) => Self::ContourFill {
891                vertices: fill
892                    .cpu_vertices()
893                    .unwrap_or(&[])
894                    .iter()
895                    .cloned()
896                    .map(Into::into)
897                    .collect(),
898                bounds_min: vec3_to_xyz(fill.bounds().min),
899                bounds_max: vec3_to_xyz(fill.bounds().max),
900                axes_index,
901                label: fill.label.clone(),
902                visible: fill.visible,
903            },
904            PlotElement::Pie(pie) => Self::Pie {
905                values: pie.values.clone(),
906                colors_rgba: pie.colors.iter().map(|c| vec4_to_rgba(*c)).collect(),
907                slice_labels: pie.slice_labels.clone(),
908                label_format: pie.label_format.clone(),
909                explode: pie.explode.clone(),
910                axes_index,
911                label: pie.label.clone(),
912                visible: pie.visible,
913            },
914        }
915    }
916
917    fn apply_to_figure(self, figure: &mut Figure) -> Result<(), String> {
918        match self {
919            ScenePlot::Line {
920                x,
921                y,
922                color_rgba,
923                line_width,
924                line_style,
925                axes_index,
926                label,
927                visible,
928            } => {
929                let mut line = LinePlot::new(x, y)?;
930                line.set_color(rgba_to_vec4(color_rgba));
931                line.set_line_width(line_width);
932                line.set_line_style(parse_line_style(&line_style));
933                line.label = label;
934                line.set_visible(visible);
935                figure.add_line_plot_on_axes(line, axes_index as usize);
936            }
937            ScenePlot::Scatter {
938                x,
939                y,
940                color_rgba,
941                marker_size,
942                marker_style,
943                axes_index,
944                label,
945                visible,
946            } => {
947                let mut scatter = ScatterPlot::new(x, y)?;
948                scatter.set_color(rgba_to_vec4(color_rgba));
949                scatter.set_marker_size(marker_size);
950                scatter.set_marker_style(parse_marker_style(&marker_style));
951                scatter.label = label;
952                scatter.set_visible(visible);
953                figure.add_scatter_plot_on_axes(scatter, axes_index as usize);
954            }
955            ScenePlot::Bar {
956                labels,
957                values,
958                histogram_bin_edges,
959                color_rgba,
960                outline_color_rgba,
961                bar_width,
962                outline_width,
963                orientation,
964                group_index,
965                group_count,
966                stack_offsets,
967                axes_index,
968                label,
969                visible,
970            } => {
971                let mut bar = BarChart::new(labels, values)?
972                    .with_style(rgba_to_vec4(color_rgba), bar_width)
973                    .with_orientation(parse_bar_orientation(&orientation))
974                    .with_group(group_index as usize, group_count as usize);
975                if let Some(edges) = histogram_bin_edges {
976                    bar.set_histogram_bin_edges(edges);
977                }
978                if let Some(offsets) = stack_offsets {
979                    bar = bar.with_stack_offsets(offsets);
980                }
981                if let Some(outline) = outline_color_rgba {
982                    bar = bar.with_outline(rgba_to_vec4(outline), outline_width);
983                }
984                bar.label = label;
985                bar.set_visible(visible);
986                figure.add_bar_chart_on_axes(bar, axes_index as usize);
987            }
988            ScenePlot::ErrorBar {
989                x,
990                y,
991                err_low,
992                err_high,
993                x_err_low,
994                x_err_high,
995                orientation,
996                color_rgba,
997                line_width,
998                line_style,
999                cap_width,
1000                marker_style,
1001                marker_size,
1002                marker_face_color,
1003                marker_edge_color,
1004                marker_filled,
1005                axes_index,
1006                label,
1007                visible,
1008            } => {
1009                let mut error = if orientation.eq_ignore_ascii_case("Both") {
1010                    ErrorBar::new_both(x, y, x_err_low, x_err_high, err_low, err_high)?
1011                } else {
1012                    ErrorBar::new_vertical(x, y, err_low, err_high)?
1013                }
1014                .with_style(
1015                    rgba_to_vec4(color_rgba),
1016                    line_width,
1017                    parse_line_style_name(&line_style),
1018                    cap_width,
1019                );
1020                if let Some(size) = marker_size {
1021                    error.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1022                        kind: parse_marker_style(marker_style.as_deref().unwrap_or("Circle")),
1023                        size,
1024                        edge_color: marker_edge_color
1025                            .map(rgba_to_vec4)
1026                            .unwrap_or(rgba_to_vec4(color_rgba)),
1027                        face_color: marker_face_color
1028                            .map(rgba_to_vec4)
1029                            .unwrap_or(rgba_to_vec4(color_rgba)),
1030                        filled: marker_filled.unwrap_or(false),
1031                    }));
1032                }
1033                error.label = label;
1034                error.set_visible(visible);
1035                figure.add_errorbar_on_axes(error, axes_index as usize);
1036            }
1037            ScenePlot::Stairs {
1038                x,
1039                y,
1040                color_rgba,
1041                line_width,
1042                axes_index,
1043                label,
1044                visible,
1045            } => {
1046                let mut stairs = StairsPlot::new(x, y)?;
1047                stairs.color = rgba_to_vec4(color_rgba);
1048                stairs.line_width = line_width;
1049                stairs.label = label;
1050                stairs.set_visible(visible);
1051                figure.add_stairs_plot_on_axes(stairs, axes_index as usize);
1052            }
1053            ScenePlot::Stem {
1054                x,
1055                y,
1056                baseline,
1057                color_rgba,
1058                line_width,
1059                line_style,
1060                baseline_color_rgba,
1061                baseline_visible,
1062                marker_color_rgba,
1063                marker_size,
1064                marker_filled,
1065                axes_index,
1066                label,
1067                visible,
1068            } => {
1069                let mut stem = StemPlot::new(x, y)?;
1070                stem = stem
1071                    .with_style(
1072                        rgba_to_vec4(color_rgba),
1073                        line_width,
1074                        parse_line_style_name(&line_style),
1075                        baseline,
1076                    )
1077                    .with_baseline_style(rgba_to_vec4(baseline_color_rgba), baseline_visible);
1078                if marker_size > 0.0 {
1079                    stem.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1080                        kind: crate::plots::scatter::MarkerStyle::Circle,
1081                        size: marker_size,
1082                        edge_color: rgba_to_vec4(marker_color_rgba),
1083                        face_color: rgba_to_vec4(marker_color_rgba),
1084                        filled: marker_filled,
1085                    }));
1086                }
1087                stem.label = label;
1088                stem.set_visible(visible);
1089                figure.add_stem_plot_on_axes(stem, axes_index as usize);
1090            }
1091            ScenePlot::Area {
1092                x,
1093                y,
1094                lower_y,
1095                baseline,
1096                color_rgba,
1097                axes_index,
1098                label,
1099                visible,
1100            } => {
1101                let mut area = AreaPlot::new(x, y)?;
1102                if let Some(lower_y) = lower_y {
1103                    area = area.with_lower_curve(lower_y);
1104                }
1105                area.baseline = baseline;
1106                area.color = rgba_to_vec4(color_rgba);
1107                area.label = label;
1108                area.set_visible(visible);
1109                figure.add_area_plot_on_axes(area, axes_index as usize);
1110            }
1111            ScenePlot::Quiver {
1112                x,
1113                y,
1114                u,
1115                v,
1116                color_rgba,
1117                line_width,
1118                scale,
1119                head_size,
1120                axes_index,
1121                label,
1122                visible,
1123            } => {
1124                let mut quiver = QuiverPlot::new(x, y, u, v)?
1125                    .with_style(rgba_to_vec4(color_rgba), line_width, scale, head_size)
1126                    .with_label(label.unwrap_or_else(|| "Data".to_string()));
1127                quiver.set_visible(visible);
1128                figure.add_quiver_plot_on_axes(quiver, axes_index as usize);
1129            }
1130            ScenePlot::Surface {
1131                x,
1132                y,
1133                z,
1134                colormap,
1135                shading_mode,
1136                wireframe,
1137                alpha,
1138                flatten_z,
1139                image_mode,
1140                color_grid_rgba,
1141                color_limits,
1142                axes_index,
1143                label,
1144                visible,
1145            } => {
1146                let mut surface = SurfacePlot::new(x, y, z)?;
1147                surface.colormap = parse_colormap(&colormap);
1148                surface.shading_mode = parse_shading_mode(&shading_mode);
1149                surface.wireframe = wireframe;
1150                surface.alpha = alpha.clamp(0.0, 1.0);
1151                surface.flatten_z = flatten_z;
1152                surface.image_mode = image_mode;
1153                surface.color_grid = color_grid_rgba.map(|grid| {
1154                    grid.into_iter()
1155                        .map(|row| row.into_iter().map(rgba_to_vec4).collect())
1156                        .collect()
1157                });
1158                surface.color_limits = color_limits.map(|[lo, hi]| (lo, hi));
1159                surface.label = label;
1160                surface.visible = visible;
1161                figure.add_surface_plot_on_axes(surface, axes_index as usize);
1162            }
1163            ScenePlot::Line3 {
1164                x,
1165                y,
1166                z,
1167                color_rgba,
1168                line_width,
1169                line_style,
1170                axes_index,
1171                label,
1172                visible,
1173            } => {
1174                let mut plot = Line3Plot::new(x, y, z)?
1175                    .with_style(
1176                        rgba_to_vec4(color_rgba),
1177                        line_width,
1178                        parse_line_style_name(&line_style),
1179                    )
1180                    .with_label(label.unwrap_or_else(|| "Data".to_string()));
1181                plot.set_visible(visible);
1182                figure.add_line3_plot_on_axes(plot, axes_index as usize);
1183            }
1184            ScenePlot::Scatter3 {
1185                points,
1186                colors_rgba,
1187                point_size,
1188                point_sizes,
1189                axes_index,
1190                label,
1191                visible,
1192            } => {
1193                let points: Vec<Vec3> = points.into_iter().map(xyz_to_vec3).collect();
1194                let colors: Vec<Vec4> = colors_rgba.into_iter().map(rgba_to_vec4).collect();
1195                let mut scatter3 = Scatter3Plot::new(points)?;
1196                if !colors.is_empty() {
1197                    scatter3 = scatter3.with_colors(colors)?;
1198                }
1199                scatter3.point_size = point_size.max(1.0);
1200                scatter3.point_sizes = point_sizes;
1201                scatter3.label = label;
1202                scatter3.visible = visible;
1203                figure.add_scatter3_plot_on_axes(scatter3, axes_index as usize);
1204            }
1205            ScenePlot::Contour {
1206                vertices,
1207                bounds_min,
1208                bounds_max,
1209                base_z,
1210                line_width,
1211                axes_index,
1212                label,
1213                visible,
1214            } => {
1215                let mut contour = ContourPlot::from_vertices(
1216                    vertices.into_iter().map(Into::into).collect(),
1217                    base_z,
1218                    serialized_bounds(bounds_min, bounds_max),
1219                )
1220                .with_line_width(line_width);
1221                contour.label = label;
1222                contour.set_visible(visible);
1223                figure.add_contour_plot_on_axes(contour, axes_index as usize);
1224            }
1225            ScenePlot::ContourFill {
1226                vertices,
1227                bounds_min,
1228                bounds_max,
1229                axes_index,
1230                label,
1231                visible,
1232            } => {
1233                let mut fill = ContourFillPlot::from_vertices(
1234                    vertices.into_iter().map(Into::into).collect(),
1235                    serialized_bounds(bounds_min, bounds_max),
1236                );
1237                fill.label = label;
1238                fill.set_visible(visible);
1239                figure.add_contour_fill_plot_on_axes(fill, axes_index as usize);
1240            }
1241            ScenePlot::Pie {
1242                values,
1243                colors_rgba,
1244                slice_labels,
1245                label_format,
1246                explode,
1247                axes_index,
1248                label,
1249                visible,
1250            } => {
1251                let mut pie = crate::plots::PieChart::new(
1252                    values,
1253                    Some(colors_rgba.into_iter().map(rgba_to_vec4).collect()),
1254                )?
1255                .with_slice_labels(slice_labels)
1256                .with_explode(explode);
1257                if let Some(fmt) = label_format {
1258                    pie = pie.with_label_format(fmt);
1259                }
1260                pie.label = label;
1261                pie.set_visible(visible);
1262                figure.add_pie_chart_on_axes(pie, axes_index as usize);
1263            }
1264            ScenePlot::Unsupported { .. } => {}
1265        }
1266        Ok(())
1267    }
1268}
1269
1270fn parse_line_style(value: &str) -> crate::plots::LineStyle {
1271    match value {
1272        "Dashed" => crate::plots::LineStyle::Dashed,
1273        "Dotted" => crate::plots::LineStyle::Dotted,
1274        "DashDot" => crate::plots::LineStyle::DashDot,
1275        _ => crate::plots::LineStyle::Solid,
1276    }
1277}
1278
1279fn parse_bar_orientation(value: &str) -> crate::plots::bar::Orientation {
1280    match value {
1281        "Horizontal" => crate::plots::bar::Orientation::Horizontal,
1282        _ => crate::plots::bar::Orientation::Vertical,
1283    }
1284}
1285
1286fn parse_marker_style(value: &str) -> MarkerStyle {
1287    match value {
1288        "Square" => MarkerStyle::Square,
1289        "Triangle" => MarkerStyle::Triangle,
1290        "Diamond" => MarkerStyle::Diamond,
1291        "Plus" => MarkerStyle::Plus,
1292        "Cross" => MarkerStyle::Cross,
1293        "Star" => MarkerStyle::Star,
1294        "Hexagon" => MarkerStyle::Hexagon,
1295        _ => MarkerStyle::Circle,
1296    }
1297}
1298
1299fn parse_colormap(value: &str) -> ColorMap {
1300    match value {
1301        "Jet" => ColorMap::Jet,
1302        "Hot" => ColorMap::Hot,
1303        "Cool" => ColorMap::Cool,
1304        "Spring" => ColorMap::Spring,
1305        "Summer" => ColorMap::Summer,
1306        "Autumn" => ColorMap::Autumn,
1307        "Winter" => ColorMap::Winter,
1308        "Gray" => ColorMap::Gray,
1309        "Bone" => ColorMap::Bone,
1310        "Copper" => ColorMap::Copper,
1311        "Pink" => ColorMap::Pink,
1312        "Lines" => ColorMap::Lines,
1313        "Viridis" => ColorMap::Viridis,
1314        "Plasma" => ColorMap::Plasma,
1315        "Inferno" => ColorMap::Inferno,
1316        "Magma" => ColorMap::Magma,
1317        "Turbo" => ColorMap::Turbo,
1318        "Parula" => ColorMap::Parula,
1319        _ => ColorMap::Parula,
1320    }
1321}
1322
1323fn parse_shading_mode(value: &str) -> ShadingMode {
1324    match value {
1325        "Flat" => ShadingMode::Flat,
1326        "Smooth" => ShadingMode::Smooth,
1327        "Faceted" => ShadingMode::Faceted,
1328        "None" => ShadingMode::None,
1329        _ => ShadingMode::Smooth,
1330    }
1331}
1332
1333fn xyz_to_vec3(value: [f32; 3]) -> Vec3 {
1334    Vec3::new(value[0], value[1], value[2])
1335}
1336
1337fn serialized_bounds(min: [f32; 3], max: [f32; 3]) -> BoundingBox {
1338    BoundingBox::new(xyz_to_vec3(min), xyz_to_vec3(max))
1339}
1340
1341fn vec3_to_xyz(value: Vec3) -> [f32; 3] {
1342    [value.x, value.y, value.z]
1343}
1344
1345fn rgba_to_vec4(value: [f32; 4]) -> Vec4 {
1346    Vec4::new(value[0], value[1], value[2], value[3])
1347}
1348
1349#[derive(Debug, Clone, Serialize, Deserialize)]
1350#[serde(rename_all = "camelCase")]
1351pub struct SerializedVertex {
1352    position: [f32; 3],
1353    color_rgba: [f32; 4],
1354    normal: [f32; 3],
1355    tex_coords: [f32; 2],
1356}
1357
1358impl From<Vertex> for SerializedVertex {
1359    fn from(value: Vertex) -> Self {
1360        Self {
1361            position: value.position,
1362            color_rgba: value.color,
1363            normal: value.normal,
1364            tex_coords: value.tex_coords,
1365        }
1366    }
1367}
1368
1369impl From<SerializedVertex> for Vertex {
1370    fn from(value: SerializedVertex) -> Self {
1371        Self {
1372            position: value.position,
1373            color: value.color_rgba,
1374            normal: value.normal,
1375            tex_coords: value.tex_coords,
1376        }
1377    }
1378}
1379
1380/// Serialized legend entry for frontend rendering.
1381#[derive(Debug, Clone, Serialize, Deserialize)]
1382#[serde(rename_all = "camelCase")]
1383pub struct FigureLegendEntry {
1384    pub label: String,
1385    pub plot_type: PlotKind,
1386    pub color_rgba: [f32; 4],
1387}
1388
1389impl From<LegendEntry> for FigureLegendEntry {
1390    fn from(entry: LegendEntry) -> Self {
1391        Self {
1392            label: entry.label,
1393            plot_type: PlotKind::from(entry.plot_type),
1394            color_rgba: vec4_to_rgba(entry.color),
1395        }
1396    }
1397}
1398
1399/// Serializable plot kind values consumed by UI + transports.
1400#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1401#[serde(rename_all = "snake_case")]
1402pub enum PlotKind {
1403    Line,
1404    Line3,
1405    Scatter,
1406    Bar,
1407    ErrorBar,
1408    Stairs,
1409    Stem,
1410    Area,
1411    Quiver,
1412    Pie,
1413    Image,
1414    Surface,
1415    Scatter3,
1416    Contour,
1417    ContourFill,
1418}
1419
1420impl From<PlotType> for PlotKind {
1421    fn from(value: PlotType) -> Self {
1422        match value {
1423            PlotType::Line => Self::Line,
1424            PlotType::Line3 => Self::Line3,
1425            PlotType::Scatter => Self::Scatter,
1426            PlotType::Bar => Self::Bar,
1427            PlotType::ErrorBar => Self::ErrorBar,
1428            PlotType::Stairs => Self::Stairs,
1429            PlotType::Stem => Self::Stem,
1430            PlotType::Area => Self::Area,
1431            PlotType::Quiver => Self::Quiver,
1432            PlotType::Pie => Self::Pie,
1433            PlotType::Surface => Self::Surface,
1434            PlotType::Scatter3 => Self::Scatter3,
1435            PlotType::Contour => Self::Contour,
1436            PlotType::ContourFill => Self::ContourFill,
1437        }
1438    }
1439}
1440
1441fn parse_line_style_name(name: &str) -> crate::plots::line::LineStyle {
1442    match name.to_ascii_lowercase().as_str() {
1443        "dashed" => crate::plots::line::LineStyle::Dashed,
1444        "dotted" => crate::plots::line::LineStyle::Dotted,
1445        "dashdot" => crate::plots::line::LineStyle::DashDot,
1446        _ => crate::plots::line::LineStyle::Solid,
1447    }
1448}
1449
1450fn parse_colormap_name(name: &str) -> crate::plots::surface::ColorMap {
1451    match name.trim().to_ascii_lowercase().as_str() {
1452        "viridis" => crate::plots::surface::ColorMap::Viridis,
1453        "plasma" => crate::plots::surface::ColorMap::Plasma,
1454        "inferno" => crate::plots::surface::ColorMap::Inferno,
1455        "magma" => crate::plots::surface::ColorMap::Magma,
1456        "turbo" => crate::plots::surface::ColorMap::Turbo,
1457        "jet" => crate::plots::surface::ColorMap::Jet,
1458        "hot" => crate::plots::surface::ColorMap::Hot,
1459        "cool" => crate::plots::surface::ColorMap::Cool,
1460        "spring" => crate::plots::surface::ColorMap::Spring,
1461        "summer" => crate::plots::surface::ColorMap::Summer,
1462        "autumn" => crate::plots::surface::ColorMap::Autumn,
1463        "winter" => crate::plots::surface::ColorMap::Winter,
1464        "gray" | "grey" => crate::plots::surface::ColorMap::Gray,
1465        "bone" => crate::plots::surface::ColorMap::Bone,
1466        "copper" => crate::plots::surface::ColorMap::Copper,
1467        "pink" => crate::plots::surface::ColorMap::Pink,
1468        "lines" => crate::plots::surface::ColorMap::Lines,
1469        _ => crate::plots::surface::ColorMap::Parula,
1470    }
1471}
1472
1473fn vec4_to_rgba(value: Vec4) -> [f32; 4] {
1474    [value.x, value.y, value.z, value.w]
1475}
1476
1477fn deserialize_f64_lossy<'de, D>(deserializer: D) -> Result<f64, D::Error>
1478where
1479    D: serde::Deserializer<'de>,
1480{
1481    let value = Option::<f64>::deserialize(deserializer)?;
1482    Ok(value.unwrap_or(f64::NAN))
1483}
1484
1485fn deserialize_vec_f64_lossy<'de, D>(deserializer: D) -> Result<Vec<f64>, D::Error>
1486where
1487    D: serde::Deserializer<'de>,
1488{
1489    let values = Vec::<Option<f64>>::deserialize(deserializer)?;
1490    Ok(values
1491        .into_iter()
1492        .map(|value| value.unwrap_or(f64::NAN))
1493        .collect())
1494}
1495
1496fn deserialize_option_vec_f64_lossy<'de, D>(deserializer: D) -> Result<Option<Vec<f64>>, D::Error>
1497where
1498    D: serde::Deserializer<'de>,
1499{
1500    let values = Option::<Vec<Option<f64>>>::deserialize(deserializer)?;
1501    Ok(values.map(|items| {
1502        items
1503            .into_iter()
1504            .map(|value| value.unwrap_or(f64::NAN))
1505            .collect()
1506    }))
1507}
1508
1509fn deserialize_matrix_f64_lossy<'de, D>(deserializer: D) -> Result<Vec<Vec<f64>>, D::Error>
1510where
1511    D: serde::Deserializer<'de>,
1512{
1513    let rows = Vec::<Vec<Option<f64>>>::deserialize(deserializer)?;
1514    Ok(rows
1515        .into_iter()
1516        .map(|row| {
1517            row.into_iter()
1518                .map(|value| value.unwrap_or(f64::NAN))
1519                .collect()
1520        })
1521        .collect())
1522}
1523
1524fn deserialize_option_pair_f64_lossy<'de, D>(deserializer: D) -> Result<Option<[f64; 2]>, D::Error>
1525where
1526    D: serde::Deserializer<'de>,
1527{
1528    let value = Option::<[Option<f64>; 2]>::deserialize(deserializer)?;
1529    Ok(value.map(|pair| [pair[0].unwrap_or(f64::NAN), pair[1].unwrap_or(f64::NAN)]))
1530}
1531
1532fn deserialize_option_vec_f32_lossy<'de, D>(deserializer: D) -> Result<Option<Vec<f32>>, D::Error>
1533where
1534    D: serde::Deserializer<'de>,
1535{
1536    let values = Option::<Vec<Option<f32>>>::deserialize(deserializer)?;
1537    Ok(values.map(|items| {
1538        items
1539            .into_iter()
1540            .map(|value| value.unwrap_or(f32::NAN))
1541            .collect()
1542    }))
1543}
1544
1545fn deserialize_vec_xyz_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<[f32; 3]>, D::Error>
1546where
1547    D: serde::Deserializer<'de>,
1548{
1549    let values = Vec::<[Option<f32>; 3]>::deserialize(deserializer)?;
1550    Ok(values
1551        .into_iter()
1552        .map(|xyz| {
1553            [
1554                xyz[0].unwrap_or(f32::NAN),
1555                xyz[1].unwrap_or(f32::NAN),
1556                xyz[2].unwrap_or(f32::NAN),
1557            ]
1558        })
1559        .collect())
1560}
1561
1562fn deserialize_vec_rgba_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<[f32; 4]>, D::Error>
1563where
1564    D: serde::Deserializer<'de>,
1565{
1566    let values = Vec::<[Option<f32>; 4]>::deserialize(deserializer)?;
1567    Ok(values
1568        .into_iter()
1569        .map(|rgba| {
1570            [
1571                rgba[0].unwrap_or(f32::NAN),
1572                rgba[1].unwrap_or(f32::NAN),
1573                rgba[2].unwrap_or(f32::NAN),
1574                rgba[3].unwrap_or(f32::NAN),
1575            ]
1576        })
1577        .collect())
1578}
1579
1580#[cfg(test)]
1581mod tests {
1582    use super::*;
1583    use crate::plots::{Figure, Line3Plot, LinePlot, Scatter3Plot, ScatterPlot, SurfacePlot};
1584    use glam::Vec3;
1585
1586    #[test]
1587    fn capture_snapshot_reflects_layout_and_metadata() {
1588        let mut figure = Figure::new()
1589            .with_title("Demo")
1590            .with_sg_title("Overview")
1591            .with_labels("X", "Y")
1592            .with_grid(false)
1593            .with_subplot_grid(1, 2);
1594        let line = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
1595        figure.add_line_plot_on_axes(line, 1);
1596
1597        let snapshot = FigureSnapshot::capture(&figure);
1598        assert_eq!(snapshot.layout.axes_rows, 1);
1599        assert_eq!(snapshot.layout.axes_cols, 2);
1600        assert_eq!(snapshot.metadata.title.as_deref(), Some("Demo"));
1601        assert_eq!(snapshot.metadata.sg_title.as_deref(), Some("Overview"));
1602        assert_eq!(snapshot.metadata.legend_entries.len(), 0);
1603        assert_eq!(snapshot.plots.len(), 1);
1604        assert_eq!(snapshot.plots[0].axes_index, 1);
1605        assert!(!snapshot.metadata.grid_enabled);
1606    }
1607
1608    #[test]
1609    fn sg_title_style_omitted_when_sg_title_absent() {
1610        let figure = Figure::new().with_title("Only regular title");
1611        let snapshot = FigureSnapshot::capture(&figure);
1612        assert!(snapshot.metadata.sg_title.is_none());
1613        assert!(
1614            snapshot.metadata.sg_title_style.is_none(),
1615            "sgTitleStyle must be None when sgTitle is absent"
1616        );
1617        let json = serde_json::to_string(&snapshot.metadata).unwrap();
1618        assert!(
1619            !json.contains("sgTitleStyle"),
1620            "sgTitleStyle must not appear in serialized JSON when sgTitle is absent"
1621        );
1622    }
1623
1624    #[test]
1625    fn figure_scene_roundtrip_reconstructs_supported_plots() {
1626        let mut figure = Figure::new().with_title("Replay").with_subplot_grid(1, 2);
1627        let mut line = LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap();
1628        line.label = Some("line".to_string());
1629        figure.add_line_plot_on_axes(line, 0);
1630        let mut scatter = ScatterPlot::new(vec![0.0, 1.0, 2.0], vec![2.0, 3.0, 4.0]).unwrap();
1631        scatter.label = Some("scatter".to_string());
1632        figure.add_scatter_plot_on_axes(scatter, 1);
1633
1634        let scene = FigureScene::capture(&figure);
1635        let rebuilt = scene.into_figure().expect("scene restore should succeed");
1636        assert_eq!(rebuilt.axes_grid(), (1, 2));
1637        assert_eq!(rebuilt.plots().count(), 2);
1638        assert_eq!(rebuilt.title.as_deref(), Some("Replay"));
1639    }
1640
1641    #[test]
1642    fn figure_scene_roundtrip_reconstructs_surface_and_scatter3() {
1643        let mut figure = Figure::new().with_title("Replay3D").with_subplot_grid(1, 2);
1644        let mut surface = SurfacePlot::new(
1645            vec![0.0, 1.0],
1646            vec![0.0, 1.0],
1647            vec![vec![0.0, 1.0], vec![1.0, 2.0]],
1648        )
1649        .expect("surface data should be valid");
1650        surface.label = Some("surface".to_string());
1651        figure.add_surface_plot_on_axes(surface, 0);
1652
1653        let mut scatter3 = Scatter3Plot::new(vec![
1654            Vec3::new(0.0, 0.0, 0.0),
1655            Vec3::new(1.0, 2.0, 3.0),
1656            Vec3::new(2.0, 3.0, 4.0),
1657        ])
1658        .expect("scatter3 data should be valid");
1659        scatter3.label = Some("scatter3".to_string());
1660        figure.add_scatter3_plot_on_axes(scatter3, 1);
1661
1662        let scene = FigureScene::capture(&figure);
1663        let rebuilt = scene.into_figure().expect("scene restore should succeed");
1664        assert_eq!(rebuilt.axes_grid(), (1, 2));
1665        assert_eq!(rebuilt.plots().count(), 2);
1666        assert_eq!(rebuilt.title.as_deref(), Some("Replay3D"));
1667        assert!(matches!(
1668            rebuilt.plots().next(),
1669            Some(PlotElement::Surface(_))
1670        ));
1671        assert!(matches!(
1672            rebuilt.plots().nth(1),
1673            Some(PlotElement::Scatter3(_))
1674        ));
1675    }
1676
1677    #[test]
1678    fn figure_scene_roundtrip_preserves_line3_plot() {
1679        let mut figure = Figure::new();
1680        let line3 = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0])
1681            .unwrap()
1682            .with_label("Trajectory");
1683        figure.add_line3_plot(line3);
1684
1685        let rebuilt = FigureScene::capture(&figure)
1686            .into_figure()
1687            .expect("scene restore should succeed");
1688
1689        let PlotElement::Line3(line3) = rebuilt.plots().next().unwrap() else {
1690            panic!("expected line3")
1691        };
1692        assert_eq!(line3.x_data, vec![0.0, 1.0]);
1693        assert_eq!(line3.z_data, vec![2.0, 3.0]);
1694        assert_eq!(line3.label.as_deref(), Some("Trajectory"));
1695    }
1696
1697    #[test]
1698    fn figure_scene_roundtrip_preserves_contour_and_fill_plots() {
1699        let mut figure = Figure::new();
1700        let bounds = BoundingBox::new(Vec3::new(-1.0, -2.0, 0.0), Vec3::new(3.0, 4.0, 0.0));
1701        let vertices = vec![Vertex {
1702            position: [0.0, 0.0, 0.0],
1703            color: [1.0, 0.0, 0.0, 1.0],
1704            normal: [0.0, 0.0, 1.0],
1705            tex_coords: [0.0, 0.0],
1706        }];
1707        let fill = ContourFillPlot::from_vertices(vertices.clone(), bounds).with_label("fill");
1708        let contour = ContourPlot::from_vertices(vertices, 0.0, bounds)
1709            .with_label("lines")
1710            .with_line_width(2.0);
1711        figure.add_contour_fill_plot(fill);
1712        figure.add_contour_plot(contour);
1713
1714        let rebuilt = FigureScene::capture(&figure)
1715            .into_figure()
1716            .expect("scene restore should succeed");
1717        assert!(matches!(
1718            rebuilt.plots().next(),
1719            Some(PlotElement::ContourFill(_))
1720        ));
1721        let Some(PlotElement::Contour(contour)) = rebuilt.plots().nth(1) else {
1722            panic!("expected contour")
1723        };
1724        assert_eq!(contour.line_width, 2.0);
1725    }
1726
1727    #[test]
1728    fn figure_scene_roundtrip_preserves_stem_style_surface() {
1729        let mut figure = Figure::new();
1730        let mut stem = StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
1731            .unwrap()
1732            .with_style(
1733                Vec4::new(1.0, 0.0, 0.0, 1.0),
1734                2.0,
1735                crate::plots::line::LineStyle::Dashed,
1736                -1.0,
1737            )
1738            .with_baseline_style(Vec4::new(0.0, 0.0, 0.0, 1.0), false)
1739            .with_label("Impulse");
1740        stem.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1741            kind: crate::plots::scatter::MarkerStyle::Square,
1742            size: 8.0,
1743            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
1744            face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
1745            filled: true,
1746        }));
1747        figure.add_stem_plot(stem);
1748
1749        let rebuilt = FigureScene::capture(&figure)
1750            .into_figure()
1751            .expect("scene restore should succeed");
1752        let PlotElement::Stem(stem) = rebuilt.plots().next().unwrap() else {
1753            panic!("expected stem")
1754        };
1755        assert_eq!(stem.baseline, -1.0);
1756        assert_eq!(stem.line_width, 2.0);
1757        assert_eq!(stem.label.as_deref(), Some("Impulse"));
1758        assert!(!stem.baseline_visible);
1759        assert!(stem.marker.as_ref().map(|m| m.filled).unwrap_or(false));
1760        assert_eq!(stem.marker.as_ref().map(|m| m.size), Some(8.0));
1761    }
1762
1763    #[test]
1764    fn figure_scene_roundtrip_preserves_bar_plot() {
1765        let mut figure = Figure::new();
1766        let bar = BarChart::new(vec!["A".into(), "B".into()], vec![2.0, 3.5])
1767            .unwrap()
1768            .with_style(Vec4::new(0.2, 0.4, 0.8, 1.0), 0.95)
1769            .with_outline(Vec4::new(0.1, 0.1, 0.1, 1.0), 1.5)
1770            .with_label("Histogram")
1771            .with_stack_offsets(vec![1.0, 0.5]);
1772        figure.add_bar_chart(bar);
1773
1774        let rebuilt = FigureScene::capture(&figure)
1775            .into_figure()
1776            .expect("scene restore should succeed");
1777        let PlotElement::Bar(bar) = rebuilt.plots().next().unwrap() else {
1778            panic!("expected bar")
1779        };
1780        assert_eq!(bar.labels, vec!["A", "B"]);
1781        assert_eq!(bar.values().unwrap_or(&[]), &[2.0, 3.5]);
1782        assert_eq!(bar.bar_width, 0.95);
1783        assert_eq!(bar.outline_width, 1.5);
1784        assert_eq!(bar.label.as_deref(), Some("Histogram"));
1785        assert_eq!(bar.stack_offsets().unwrap_or(&[]), &[1.0, 0.5]);
1786        assert!(bar.histogram_bin_edges().is_none());
1787    }
1788
1789    #[test]
1790    fn figure_scene_roundtrip_preserves_histogram_bin_edges() {
1791        let mut figure = Figure::new();
1792        let mut bar = BarChart::new(vec!["bin1".into(), "bin2".into()], vec![4.0, 5.0]).unwrap();
1793        bar.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
1794        figure.add_bar_chart(bar);
1795
1796        let rebuilt = FigureScene::capture(&figure)
1797            .into_figure()
1798            .expect("scene restore should succeed");
1799        let PlotElement::Bar(bar) = rebuilt.plots().next().unwrap() else {
1800            panic!("expected bar")
1801        };
1802        assert_eq!(bar.histogram_bin_edges().unwrap_or(&[]), &[0.0, 0.5, 1.0]);
1803    }
1804
1805    #[test]
1806    fn figure_scene_roundtrip_preserves_errorbar_style_surface() {
1807        let mut figure = Figure::new();
1808        let mut error = ErrorBar::new_vertical(
1809            vec![0.0, 1.0],
1810            vec![1.0, 2.0],
1811            vec![0.1, 0.2],
1812            vec![0.2, 0.3],
1813        )
1814        .unwrap()
1815        .with_style(
1816            Vec4::new(1.0, 0.0, 0.0, 1.0),
1817            2.0,
1818            crate::plots::line::LineStyle::Dashed,
1819            10.0,
1820        )
1821        .with_label("Err");
1822        error.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1823            kind: crate::plots::scatter::MarkerStyle::Triangle,
1824            size: 8.0,
1825            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
1826            face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
1827            filled: true,
1828        }));
1829        figure.add_errorbar(error);
1830
1831        let rebuilt = FigureScene::capture(&figure)
1832            .into_figure()
1833            .expect("scene restore should succeed");
1834        let PlotElement::ErrorBar(error) = rebuilt.plots().next().unwrap() else {
1835            panic!("expected errorbar")
1836        };
1837        assert_eq!(error.line_width, 2.0);
1838        assert_eq!(error.cap_size, 10.0);
1839        assert_eq!(error.label.as_deref(), Some("Err"));
1840        assert_eq!(error.line_style, crate::plots::line::LineStyle::Dashed);
1841        assert!(error.marker.as_ref().map(|m| m.filled).unwrap_or(false));
1842    }
1843
1844    #[test]
1845    fn figure_scene_roundtrip_preserves_errorbar_both_direction() {
1846        let mut figure = Figure::new();
1847        let error = ErrorBar::new_both(
1848            vec![1.0, 2.0],
1849            vec![3.0, 4.0],
1850            vec![0.1, 0.2],
1851            vec![0.2, 0.3],
1852            vec![0.3, 0.4],
1853            vec![0.4, 0.5],
1854        )
1855        .unwrap();
1856        figure.add_errorbar(error);
1857        let rebuilt = FigureScene::capture(&figure)
1858            .into_figure()
1859            .expect("scene restore should succeed");
1860        let PlotElement::ErrorBar(error) = rebuilt.plots().next().unwrap() else {
1861            panic!("expected errorbar")
1862        };
1863        assert_eq!(
1864            error.orientation,
1865            crate::plots::errorbar::ErrorBarOrientation::Both
1866        );
1867        assert_eq!(error.x_neg, vec![0.1, 0.2]);
1868        assert_eq!(error.x_pos, vec![0.2, 0.3]);
1869    }
1870
1871    #[test]
1872    fn figure_scene_roundtrip_preserves_quiver_plot() {
1873        let mut figure = Figure::new();
1874        let quiver = QuiverPlot::new(
1875            vec![0.0, 1.0],
1876            vec![1.0, 2.0],
1877            vec![0.5, -0.5],
1878            vec![1.0, 0.25],
1879        )
1880        .unwrap()
1881        .with_style(Vec4::new(0.2, 0.3, 0.4, 1.0), 2.0, 1.5, 0.2)
1882        .with_label("Field");
1883        figure.add_quiver_plot(quiver);
1884
1885        let rebuilt = FigureScene::capture(&figure)
1886            .into_figure()
1887            .expect("scene restore should succeed");
1888        let PlotElement::Quiver(quiver) = rebuilt.plots().next().unwrap() else {
1889            panic!("expected quiver")
1890        };
1891        assert_eq!(quiver.u, vec![0.5, -0.5]);
1892        assert_eq!(quiver.v, vec![1.0, 0.25]);
1893        assert_eq!(quiver.line_width, 2.0);
1894        assert_eq!(quiver.scale, 1.5);
1895        assert_eq!(quiver.head_size, 0.2);
1896        assert_eq!(quiver.label.as_deref(), Some("Field"));
1897    }
1898
1899    #[test]
1900    fn figure_scene_roundtrip_preserves_image_surface_mode_and_color_grid() {
1901        let mut figure = Figure::new();
1902        let surface = SurfacePlot::new(
1903            vec![0.0, 1.0],
1904            vec![0.0, 1.0],
1905            vec![vec![0.0, 0.0], vec![0.0, 0.0]],
1906        )
1907        .unwrap()
1908        .with_flatten_z(true)
1909        .with_image_mode(true)
1910        .with_color_grid(vec![
1911            vec![Vec4::new(1.0, 0.0, 0.0, 1.0), Vec4::new(0.0, 1.0, 0.0, 1.0)],
1912            vec![Vec4::new(0.0, 0.0, 1.0, 1.0), Vec4::new(1.0, 1.0, 1.0, 1.0)],
1913        ]);
1914        figure.add_surface_plot(surface);
1915
1916        let rebuilt = FigureScene::capture(&figure)
1917            .into_figure()
1918            .expect("scene restore should succeed");
1919        let PlotElement::Surface(surface) = rebuilt.plots().next().unwrap() else {
1920            panic!("expected surface")
1921        };
1922        assert!(surface.flatten_z);
1923        assert!(surface.image_mode);
1924        assert!(surface.color_grid.is_some());
1925        assert_eq!(
1926            surface.color_grid.as_ref().unwrap()[0][0],
1927            Vec4::new(1.0, 0.0, 0.0, 1.0)
1928        );
1929    }
1930
1931    #[test]
1932    fn figure_scene_roundtrip_preserves_area_lower_curve() {
1933        let mut figure = Figure::new();
1934        let area = AreaPlot::new(vec![1.0, 2.0], vec![2.0, 3.0])
1935            .unwrap()
1936            .with_lower_curve(vec![0.5, 1.0])
1937            .with_label("Stacked");
1938        figure.add_area_plot(area);
1939
1940        let rebuilt = FigureScene::capture(&figure)
1941            .into_figure()
1942            .expect("scene restore should succeed");
1943        let PlotElement::Area(area) = rebuilt.plots().next().unwrap() else {
1944            panic!("expected area")
1945        };
1946        assert_eq!(area.lower_y, Some(vec![0.5, 1.0]));
1947        assert_eq!(area.label.as_deref(), Some("Stacked"));
1948    }
1949
1950    #[test]
1951    fn figure_scene_roundtrip_preserves_axes_local_limits_and_colormap_state() {
1952        let mut figure = Figure::new().with_subplot_grid(1, 2);
1953        figure.set_axes_limits(1, Some((1.0, 2.0)), Some((3.0, 4.0)));
1954        figure.set_axes_z_limits(1, Some((5.0, 6.0)));
1955        figure.set_axes_grid_enabled(1, false);
1956        figure.set_axes_box_enabled(1, false);
1957        figure.set_axes_axis_equal(1, true);
1958        figure.set_axes_colorbar_enabled(1, true);
1959        figure.set_axes_colormap(1, ColorMap::Hot);
1960        figure.set_axes_color_limits(1, Some((0.0, 10.0)));
1961        figure.set_active_axes_index(1);
1962
1963        let rebuilt = FigureScene::capture(&figure)
1964            .into_figure()
1965            .expect("scene restore should succeed");
1966        let meta = rebuilt.axes_metadata(1).unwrap();
1967        assert_eq!(meta.x_limits, Some((1.0, 2.0)));
1968        assert_eq!(meta.y_limits, Some((3.0, 4.0)));
1969        assert_eq!(meta.z_limits, Some((5.0, 6.0)));
1970        assert!(!meta.grid_enabled);
1971        assert!(!meta.box_enabled);
1972        assert!(meta.axis_equal);
1973        assert!(meta.colorbar_enabled);
1974        assert_eq!(format!("{:?}", meta.colormap), "Hot");
1975        assert_eq!(meta.color_limits, Some((0.0, 10.0)));
1976    }
1977
1978    #[test]
1979    fn figure_scene_roundtrip_preserves_axes_local_annotation_metadata() {
1980        let mut figure = Figure::new().with_subplot_grid(1, 2);
1981        figure.set_sg_title("All Panels");
1982        figure.set_sg_title_style(TextStyle {
1983            font_weight: Some("bold".into()),
1984            font_size: Some(20.0),
1985            ..Default::default()
1986        });
1987        figure.set_active_axes_index(0);
1988        figure.set_axes_title(0, "Left");
1989        figure.set_axes_xlabel(0, "LX");
1990        figure.set_axes_ylabel(0, "LY");
1991        figure.set_axes_legend_enabled(0, false);
1992        figure.set_axes_title(1, "Right");
1993        figure.set_axes_xlabel(1, "RX");
1994        figure.set_axes_ylabel(1, "RY");
1995        figure.set_axes_legend_enabled(1, true);
1996        figure.set_axes_legend_style(
1997            1,
1998            LegendStyle {
1999                location: Some("northeast".into()),
2000                font_weight: Some("bold".into()),
2001                orientation: Some("horizontal".into()),
2002                ..Default::default()
2003            },
2004        );
2005        if let Some(meta) = figure.axes_metadata.get_mut(0) {
2006            meta.title_style.font_weight = Some("bold".into());
2007            meta.title_style.font_angle = Some("italic".into());
2008        }
2009        figure.set_active_axes_index(1);
2010
2011        let rebuilt = FigureScene::capture(&figure)
2012            .into_figure()
2013            .expect("scene restore should succeed");
2014
2015        assert_eq!(rebuilt.active_axes_index, 1);
2016        assert_eq!(rebuilt.sg_title.as_deref(), Some("All Panels"));
2017        assert_eq!(rebuilt.sg_title_style.font_weight.as_deref(), Some("bold"));
2018        assert_eq!(rebuilt.sg_title_style.font_size, Some(20.0));
2019        assert_eq!(
2020            rebuilt.axes_metadata(0).and_then(|m| m.title.as_deref()),
2021            Some("Left")
2022        );
2023        assert_eq!(
2024            rebuilt.axes_metadata(0).and_then(|m| m.x_label.as_deref()),
2025            Some("LX")
2026        );
2027        assert_eq!(
2028            rebuilt.axes_metadata(0).and_then(|m| m.y_label.as_deref()),
2029            Some("LY")
2030        );
2031        assert!(!rebuilt.axes_metadata(0).unwrap().legend_enabled);
2032        assert_eq!(
2033            rebuilt
2034                .axes_metadata(0)
2035                .unwrap()
2036                .title_style
2037                .font_weight
2038                .as_deref(),
2039            Some("bold")
2040        );
2041        assert_eq!(
2042            rebuilt
2043                .axes_metadata(0)
2044                .unwrap()
2045                .title_style
2046                .font_angle
2047                .as_deref(),
2048            Some("italic")
2049        );
2050        assert_eq!(
2051            rebuilt.axes_metadata(1).and_then(|m| m.title.as_deref()),
2052            Some("Right")
2053        );
2054        assert_eq!(
2055            rebuilt.axes_metadata(1).and_then(|m| m.x_label.as_deref()),
2056            Some("RX")
2057        );
2058        assert_eq!(
2059            rebuilt.axes_metadata(1).and_then(|m| m.y_label.as_deref()),
2060            Some("RY")
2061        );
2062        assert_eq!(
2063            rebuilt
2064                .axes_metadata(1)
2065                .unwrap()
2066                .legend_style
2067                .location
2068                .as_deref(),
2069            Some("northeast")
2070        );
2071        assert_eq!(
2072            rebuilt
2073                .axes_metadata(1)
2074                .unwrap()
2075                .legend_style
2076                .font_weight
2077                .as_deref(),
2078            Some("bold")
2079        );
2080        assert_eq!(
2081            rebuilt
2082                .axes_metadata(1)
2083                .unwrap()
2084                .legend_style
2085                .orientation
2086                .as_deref(),
2087            Some("horizontal")
2088        );
2089    }
2090
2091    #[test]
2092    fn figure_scene_roundtrip_preserves_axes_local_log_modes() {
2093        let mut figure = Figure::new().with_subplot_grid(1, 2);
2094        figure.set_axes_log_modes(0, true, false);
2095        figure.set_axes_log_modes(1, false, true);
2096        figure.set_active_axes_index(1);
2097
2098        let rebuilt = FigureScene::capture(&figure)
2099            .into_figure()
2100            .expect("scene restore should succeed");
2101
2102        assert!(rebuilt.axes_metadata(0).unwrap().x_log);
2103        assert!(!rebuilt.axes_metadata(0).unwrap().y_log);
2104        assert!(!rebuilt.axes_metadata(1).unwrap().x_log);
2105        assert!(rebuilt.axes_metadata(1).unwrap().y_log);
2106        assert!(!rebuilt.x_log);
2107        assert!(rebuilt.y_log);
2108    }
2109
2110    #[test]
2111    fn figure_scene_roundtrip_preserves_zlabel_and_view_state() {
2112        let mut figure = Figure::new().with_subplot_grid(1, 2);
2113        figure.set_axes_zlabel(1, "Height");
2114        figure.set_axes_view(1, 45.0, 20.0);
2115        figure.set_active_axes_index(1);
2116
2117        let rebuilt = FigureScene::capture(&figure)
2118            .into_figure()
2119            .expect("scene restore should succeed");
2120
2121        assert_eq!(
2122            rebuilt.axes_metadata(1).unwrap().z_label.as_deref(),
2123            Some("Height")
2124        );
2125        assert_eq!(
2126            rebuilt.axes_metadata(1).unwrap().view_azimuth_deg,
2127            Some(45.0)
2128        );
2129        assert_eq!(
2130            rebuilt.axes_metadata(1).unwrap().view_elevation_deg,
2131            Some(20.0)
2132        );
2133        assert_eq!(rebuilt.z_label.as_deref(), Some("Height"));
2134    }
2135
2136    #[test]
2137    fn figure_scene_roundtrip_preserves_pie_metadata() {
2138        let mut figure = Figure::new();
2139        let pie = crate::plots::PieChart::new(vec![1.0, 2.0], None)
2140            .unwrap()
2141            .with_slice_labels(vec!["A".into(), "B".into()])
2142            .with_explode(vec![false, true]);
2143        figure.add_pie_chart(pie);
2144
2145        let rebuilt = FigureScene::capture(&figure)
2146            .into_figure()
2147            .expect("scene restore should succeed");
2148        let crate::plots::figure::PlotElement::Pie(pie) = rebuilt.plots().next().unwrap() else {
2149            panic!("expected pie")
2150        };
2151        assert_eq!(pie.slice_labels, vec!["A", "B"]);
2152        assert_eq!(pie.explode, vec![false, true]);
2153    }
2154
2155    #[test]
2156    fn scene_plot_deserialize_maps_null_numeric_values_to_nan() {
2157        let json = r#"{
2158          "schemaVersion": 1,
2159          "layout": { "axesRows": 1, "axesCols": 1, "axesIndices": [0] },
2160          "metadata": {
2161            "gridEnabled": true,
2162            "legendEnabled": false,
2163            "colorbarEnabled": false,
2164            "axisEqual": false,
2165            "backgroundRgba": [1,1,1,1],
2166            "legendEntries": []
2167          },
2168          "plots": [
2169            {
2170              "kind": "surface",
2171              "x": [0.0, null],
2172              "y": [0.0, 1.0],
2173              "z": [[0.0, null], [1.0, 2.0]],
2174              "colormap": "Parula",
2175              "shading_mode": "Smooth",
2176              "wireframe": false,
2177              "alpha": 1.0,
2178              "flatten_z": false,
2179              "color_limits": null,
2180              "axes_index": 0,
2181              "label": null,
2182              "visible": true
2183            }
2184          ]
2185        }"#;
2186        let scene: FigureScene = serde_json::from_str(json).expect("scene should deserialize");
2187        let ScenePlot::Surface { x, z, .. } = &scene.plots[0] else {
2188            panic!("expected surface plot");
2189        };
2190        assert!(x[1].is_nan());
2191        assert!(z[0][1].is_nan());
2192    }
2193
2194    #[test]
2195    fn scene_plot_deserialize_maps_null_scatter3_components_to_nan() {
2196        let json = r#"{
2197          "schemaVersion": 1,
2198          "layout": { "axesRows": 1, "axesCols": 1, "axesIndices": [0] },
2199          "metadata": {
2200            "gridEnabled": true,
2201            "legendEnabled": false,
2202            "colorbarEnabled": false,
2203            "axisEqual": false,
2204            "backgroundRgba": [1,1,1,1],
2205            "legendEntries": []
2206          },
2207          "plots": [
2208            {
2209              "kind": "scatter3",
2210              "points": [[0.0, 1.0, null], [1.0, null, 2.0]],
2211              "colors_rgba": [[0.2, 0.4, 0.6, 1.0], [0.1, 0.2, 0.3, 1.0]],
2212              "point_size": 6.0,
2213              "point_sizes": [3.0, null],
2214              "axes_index": 0,
2215              "label": null,
2216              "visible": true
2217            }
2218          ]
2219        }"#;
2220        let scene: FigureScene = serde_json::from_str(json).expect("scene should deserialize");
2221        let ScenePlot::Scatter3 {
2222            points,
2223            point_sizes,
2224            ..
2225        } = &scene.plots[0]
2226        else {
2227            panic!("expected scatter3 plot");
2228        };
2229        assert!(points[0][2].is_nan());
2230        assert!(points[1][1].is_nan());
2231        assert!(point_sizes.as_ref().unwrap()[1].is_nan());
2232    }
2233}