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