Skip to main content

runmat_plot/
event.rs

1use crate::core::{BoundingBox, Vertex};
2use crate::plots::{
3    AreaPlot, AxesKind, AxesMetadata, BarChart, ColorMap, ContourFillPlot, ContourPlot, ErrorBar,
4    Figure, LegendEntry, LegendStyle, Line3Plot, LinePlot, MarkerStyle, MeshDeformation,
5    MeshEdgeMode, MeshFieldLocation, MeshPlot, MeshRegion, MeshScalarField, MeshTriangleRange,
6    MeshVectorField, PatchEdgeColorMode, PatchFaceColorMode, PatchPlot, PlotElement, PlotType,
7    QuiverPlot, ReferenceLine, ReferenceLineOrientation, Scatter3Plot, ScatterPlot, ShadingMode,
8    StairsPlot, StemPlot, SurfacePlot, TextStyle,
9};
10use glam::{Vec3, Vec4};
11use serde::{Deserialize, Serialize};
12use std::{error::Error, fmt};
13
14/// High-level event emitted whenever a figure changes.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct FigureEvent {
18    pub handle: u32,
19    pub kind: FigureEventKind,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub fingerprint: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub figure: Option<FigureSnapshot>,
24}
25
26/// Event kind for figure lifecycle + updates.
27#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "lowercase")]
29pub enum FigureEventKind {
30    Created,
31    Updated,
32    Cleared,
33    Closed,
34}
35
36/// Snapshot of the figure state describing layout + plots.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct FigureSnapshot {
40    pub layout: FigureLayout,
41    pub metadata: FigureMetadata,
42    pub plots: Vec<PlotDescriptor>,
43}
44
45/// Full replay scene payload capable of reconstructing a figure.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct FigureScene {
49    pub schema_version: u32,
50    pub layout: FigureLayout,
51    pub metadata: FigureMetadata,
52    pub plots: Vec<ScenePlot>,
53}
54
55pub const DEFAULT_FIGURE_SCENE_EXPORT_BUDGET_BYTES: usize = 8 * 1024 * 1024;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct SceneExportPolicy {
59    pub max_scene_bytes: usize,
60}
61
62impl Default for SceneExportPolicy {
63    fn default() -> Self {
64        Self {
65            max_scene_bytes: DEFAULT_FIGURE_SCENE_EXPORT_BUDGET_BYTES,
66        }
67    }
68}
69
70pub fn resolve_scene_export_policy(max_scene_bytes: Option<usize>) -> SceneExportPolicy {
71    SceneExportPolicy {
72        max_scene_bytes: max_scene_bytes
73            .filter(|bytes| *bytes > 0)
74            .unwrap_or(DEFAULT_FIGURE_SCENE_EXPORT_BUDGET_BYTES),
75    }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum SceneExportErrorKind {
80    BudgetExceeded,
81    UnexportableGpuData,
82    Serialization,
83    Readback,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct SceneExportError {
88    pub kind: SceneExportErrorKind,
89    pub message: String,
90}
91
92impl SceneExportError {
93    fn budget_exceeded(used: usize, added: usize, max: usize) -> Self {
94        Self {
95            kind: SceneExportErrorKind::BudgetExceeded,
96            message: format!(
97                "figure scene export exceeds budget: {} + {} bytes > {} bytes",
98                used, added, max
99            ),
100        }
101    }
102
103    fn unexportable(message: impl Into<String>) -> Self {
104        Self {
105            kind: SceneExportErrorKind::UnexportableGpuData,
106            message: message.into(),
107        }
108    }
109
110    fn serialization(message: impl Into<String>) -> Self {
111        Self {
112            kind: SceneExportErrorKind::Serialization,
113            message: message.into(),
114        }
115    }
116
117    fn readback(message: impl Into<String>) -> Self {
118        Self {
119            kind: SceneExportErrorKind::Readback,
120            message: message.into(),
121        }
122    }
123}
124
125impl fmt::Display for SceneExportError {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        f.write_str(&self.message)
128    }
129}
130
131impl Error for SceneExportError {}
132
133#[derive(Debug, Clone, Copy)]
134struct SceneExportBudget {
135    max_bytes: usize,
136    used_bytes: usize,
137}
138
139impl SceneExportBudget {
140    fn new(policy: SceneExportPolicy) -> Self {
141        Self {
142            max_bytes: policy.max_scene_bytes,
143            used_bytes: 0,
144        }
145    }
146
147    fn reserve_plot(&mut self, plot: &ScenePlot) -> Result<(), SceneExportError> {
148        let bytes = serde_json::to_vec(plot)
149            .map_err(|err| SceneExportError::serialization(err.to_string()))?;
150        self.reserve_bytes(bytes.len())
151    }
152
153    fn reserve_bytes(&mut self, byte_len: usize) -> Result<(), SceneExportError> {
154        let next = self.used_bytes.saturating_add(byte_len);
155        if next > self.max_bytes {
156            return Err(SceneExportError::budget_exceeded(
157                self.used_bytes,
158                byte_len,
159                self.max_bytes,
160            ));
161        }
162        self.used_bytes = next;
163        Ok(())
164    }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168#[serde(tag = "kind", rename_all = "snake_case")]
169pub enum ScenePlot {
170    Line {
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        color_rgba: [f32; 4],
176        line_width: f32,
177        line_style: String,
178        axes_index: u32,
179        label: Option<String>,
180        visible: bool,
181    },
182    ReferenceLine {
183        orientation: String,
184        #[serde(deserialize_with = "deserialize_f64_lossy")]
185        value: f64,
186        color_rgba: [f32; 4],
187        line_width: f32,
188        line_style: String,
189        label: Option<String>,
190        display_name: Option<String>,
191        label_orientation: String,
192        axes_index: u32,
193        visible: bool,
194    },
195    Scatter {
196        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
197        x: Vec<f64>,
198        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
199        y: Vec<f64>,
200        color_rgba: [f32; 4],
201        marker_size: f32,
202        marker_style: String,
203        axes_index: u32,
204        label: Option<String>,
205        visible: bool,
206    },
207    Bar {
208        labels: Vec<String>,
209        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
210        values: Vec<f64>,
211        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
212        histogram_bin_edges: Option<Vec<f64>>,
213        color_rgba: [f32; 4],
214        #[serde(default)]
215        outline_color_rgba: Option<[f32; 4]>,
216        bar_width: f32,
217        outline_width: f32,
218        orientation: String,
219        group_index: u32,
220        group_count: u32,
221        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
222        stack_offsets: Option<Vec<f64>>,
223        axes_index: u32,
224        label: Option<String>,
225        visible: bool,
226    },
227    ErrorBar {
228        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
229        x: Vec<f64>,
230        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
231        y: Vec<f64>,
232        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
233        err_low: Vec<f64>,
234        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
235        err_high: Vec<f64>,
236        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
237        x_err_low: Vec<f64>,
238        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
239        x_err_high: Vec<f64>,
240        orientation: String,
241        color_rgba: [f32; 4],
242        line_width: f32,
243        line_style: String,
244        cap_width: f32,
245        marker_style: Option<String>,
246        marker_size: Option<f32>,
247        marker_face_color: Option<[f32; 4]>,
248        marker_edge_color: Option<[f32; 4]>,
249        marker_filled: Option<bool>,
250        axes_index: u32,
251        label: Option<String>,
252        visible: bool,
253    },
254    Stairs {
255        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
256        x: Vec<f64>,
257        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
258        y: Vec<f64>,
259        color_rgba: [f32; 4],
260        line_width: f32,
261        axes_index: u32,
262        label: Option<String>,
263        visible: bool,
264    },
265    Stem {
266        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
267        x: Vec<f64>,
268        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
269        y: Vec<f64>,
270        #[serde(deserialize_with = "deserialize_f64_lossy")]
271        baseline: f64,
272        color_rgba: [f32; 4],
273        line_width: f32,
274        line_style: String,
275        baseline_color_rgba: [f32; 4],
276        baseline_visible: bool,
277        marker_color_rgba: [f32; 4],
278        marker_size: f32,
279        marker_filled: bool,
280        axes_index: u32,
281        label: Option<String>,
282        visible: bool,
283    },
284    Area {
285        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
286        x: Vec<f64>,
287        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
288        y: Vec<f64>,
289        #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
290        lower_y: Option<Vec<f64>>,
291        #[serde(deserialize_with = "deserialize_f64_lossy")]
292        baseline: f64,
293        color_rgba: [f32; 4],
294        axes_index: u32,
295        label: Option<String>,
296        visible: bool,
297    },
298    Quiver {
299        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
300        x: Vec<f64>,
301        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
302        y: Vec<f64>,
303        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
304        u: Vec<f64>,
305        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
306        v: Vec<f64>,
307        color_rgba: [f32; 4],
308        line_width: f32,
309        scale: f32,
310        head_size: f32,
311        axes_index: u32,
312        label: Option<String>,
313        visible: bool,
314    },
315    Surface {
316        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
317        x: Vec<f64>,
318        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
319        y: Vec<f64>,
320        #[serde(deserialize_with = "deserialize_matrix_f64_lossy")]
321        z: Vec<Vec<f64>>,
322        colormap: String,
323        shading_mode: String,
324        wireframe: bool,
325        alpha: f32,
326        flatten_z: bool,
327        #[serde(default)]
328        image_mode: bool,
329        #[serde(default)]
330        color_grid_rgba: Option<Vec<Vec<[f32; 4]>>>,
331        #[serde(default, deserialize_with = "deserialize_option_pair_f64_lossy")]
332        color_limits: Option<[f64; 2]>,
333        axes_index: u32,
334        label: Option<String>,
335        visible: bool,
336    },
337    Patch {
338        #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
339        vertices: Vec<[f32; 3]>,
340        faces: Vec<Vec<u32>>,
341        face_color_rgba: [f32; 4],
342        edge_color_rgba: [f32; 4],
343        face_color_mode: String,
344        edge_color_mode: String,
345        face_alpha: f32,
346        edge_alpha: f32,
347        line_width: f32,
348        axes_index: u32,
349        label: Option<String>,
350        visible: bool,
351        #[serde(default)]
352        force_3d: bool,
353    },
354    Mesh {
355        #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
356        vertices: Vec<[f32; 3]>,
357        triangles: Vec<[u32; 3]>,
358        mesh_id: Option<String>,
359        face_color_rgba: [f32; 4],
360        edge_color_rgba: [f32; 4],
361        face_alpha: f32,
362        edge_alpha: f32,
363        edge_width: f32,
364        #[serde(default)]
365        edge_mode: String,
366        #[serde(default, skip_serializing_if = "Vec::is_empty")]
367        feature_edge_groups: Vec<u64>,
368        #[serde(default, skip_serializing_if = "Vec::is_empty")]
369        vertex_colors_rgba: Vec<[f32; 4]>,
370        #[serde(default, skip_serializing_if = "Vec::is_empty")]
371        triangle_colors_rgba: Vec<[f32; 4]>,
372        axes_index: u32,
373        label: Option<String>,
374        #[serde(default, skip_serializing_if = "Vec::is_empty")]
375        regions: Vec<SerializedMeshRegion>,
376        #[serde(default, skip_serializing_if = "Option::is_none")]
377        highlighted_region_id: Option<String>,
378        #[serde(default, skip_serializing_if = "Option::is_none")]
379        highlight_color_rgba: Option<[f32; 4]>,
380        #[serde(default, skip_serializing_if = "Option::is_none")]
381        scalar_field: Option<Box<SerializedMeshScalarField>>,
382        #[serde(default, skip_serializing_if = "Option::is_none")]
383        vector_field: Option<Box<SerializedMeshVectorField>>,
384        #[serde(default, skip_serializing_if = "Option::is_none")]
385        deformation: Option<Box<SerializedMeshDeformation>>,
386        visible: bool,
387    },
388    Line3 {
389        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
390        x: Vec<f64>,
391        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
392        y: Vec<f64>,
393        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
394        z: Vec<f64>,
395        color_rgba: [f32; 4],
396        line_width: f32,
397        line_style: String,
398        axes_index: u32,
399        label: Option<String>,
400        visible: bool,
401    },
402    Scatter3 {
403        #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
404        points: Vec<[f32; 3]>,
405        #[serde(default, deserialize_with = "deserialize_vec_rgba_f32_lossy")]
406        colors_rgba: Vec<[f32; 4]>,
407        point_size: f32,
408        #[serde(default, deserialize_with = "deserialize_option_vec_f32_lossy")]
409        point_sizes: Option<Vec<f32>>,
410        axes_index: u32,
411        label: Option<String>,
412        visible: bool,
413    },
414    Contour {
415        vertices: Vec<SerializedVertex>,
416        bounds_min: [f32; 3],
417        bounds_max: [f32; 3],
418        base_z: f32,
419        line_width: f32,
420        axes_index: u32,
421        label: Option<String>,
422        visible: bool,
423        #[serde(default)]
424        force_3d: bool,
425    },
426    ContourFill {
427        vertices: Vec<SerializedVertex>,
428        bounds_min: [f32; 3],
429        bounds_max: [f32; 3],
430        axes_index: u32,
431        label: Option<String>,
432        visible: bool,
433    },
434    Pie {
435        #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
436        values: Vec<f64>,
437        colors_rgba: Vec<[f32; 4]>,
438        slice_labels: Vec<String>,
439        label_format: Option<String>,
440        explode: Vec<bool>,
441        axes_index: u32,
442        label: Option<String>,
443        visible: bool,
444    },
445    Unsupported {
446        plot_kind: PlotKind,
447        axes_index: u32,
448        label: Option<String>,
449        visible: bool,
450    },
451}
452
453impl FigureSnapshot {
454    /// Capture a snapshot from a [`Figure`] reference.
455    pub fn capture(figure: &Figure) -> Self {
456        let (rows, cols) = figure.axes_grid();
457        let layout = FigureLayout {
458            axes_rows: rows as u32,
459            axes_cols: cols as u32,
460            axes_indices: figure
461                .plot_axes_indices()
462                .iter()
463                .map(|idx| *idx as u32)
464                .collect(),
465        };
466
467        let metadata = FigureMetadata::from_figure(figure);
468
469        let plots = figure
470            .plots()
471            .enumerate()
472            .map(|(idx, plot)| PlotDescriptor::from_plot(plot, figure_axis_index(figure, idx)))
473            .collect();
474
475        Self {
476            layout,
477            metadata,
478            plots,
479        }
480    }
481
482    pub fn fingerprint(&self) -> String {
483        const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
484        const FNV_PRIME: u64 = 0x100000001b3;
485
486        let bytes = serde_json::to_vec(self).unwrap_or_default();
487        let mut hash = FNV_OFFSET_BASIS;
488        for byte in bytes {
489            hash ^= u64::from(byte);
490            hash = hash.wrapping_mul(FNV_PRIME);
491        }
492        format!("fig:{hash:016x}")
493    }
494}
495
496impl FigureScene {
497    pub const SCHEMA_VERSION: u32 = 3;
498
499    pub fn capture(figure: &Figure) -> Self {
500        let snapshot = FigureSnapshot::capture(figure);
501        let plots = figure
502            .plots()
503            .enumerate()
504            .map(|(idx, plot)| ScenePlot::from_plot(plot, figure_axis_index(figure, idx)))
505            .collect();
506
507        Self {
508            schema_version: Self::SCHEMA_VERSION,
509            layout: snapshot.layout,
510            metadata: snapshot.metadata,
511            plots,
512        }
513    }
514
515    pub async fn capture_for_export(
516        figure: &Figure,
517        policy: SceneExportPolicy,
518    ) -> Result<Self, SceneExportError> {
519        let snapshot = FigureSnapshot::capture(figure);
520        let mut budget = SceneExportBudget::new(policy);
521        let mut plots = Vec::new();
522
523        for (idx, plot) in figure.plots().enumerate() {
524            let scene_plot =
525                ScenePlot::from_plot_for_export(plot, figure_axis_index(figure, idx)).await?;
526            budget.reserve_plot(&scene_plot)?;
527            plots.push(scene_plot);
528        }
529
530        Ok(Self {
531            schema_version: Self::SCHEMA_VERSION,
532            layout: snapshot.layout,
533            metadata: snapshot.metadata,
534            plots,
535        })
536    }
537
538    pub fn from_geometry_scene(scene: &crate::geometry_scene::GeometryScene) -> Self {
539        let mut figure = Figure::new()
540            .with_grid(scene.show_grid)
541            .with_legend(false)
542            .with_axis_equal(scene.axis_equal);
543        figure.title = scene.title.clone();
544        figure.x_label = Some("X".to_string());
545        figure.y_label = Some("Y".to_string());
546        figure.z_label = Some("Z".to_string());
547        figure.set_axes_view(0, -38.0, 24.0);
548        let snapshot = FigureSnapshot::capture(&figure);
549        let plots = scene
550            .chunks
551            .iter()
552            .filter_map(scene_chunk_to_mesh_plot)
553            .collect::<Vec<_>>();
554
555        Self {
556            schema_version: Self::SCHEMA_VERSION,
557            layout: FigureLayout {
558                axes_rows: 1,
559                axes_cols: 1,
560                axes_indices: vec![0; plots.len()],
561            },
562            metadata: snapshot.metadata,
563            plots,
564        }
565    }
566
567    pub fn into_figure(self) -> Result<Figure, String> {
568        self.validate_schema_version()?;
569
570        let mut figure = Figure::new();
571        figure.set_subplot_grid(
572            self.layout.axes_rows as usize,
573            self.layout.axes_cols as usize,
574        );
575        figure.active_axes_index = self.metadata.active_axes_index as usize;
576        if let Some(axes_metadata) = self.metadata.axes_metadata.clone() {
577            figure.axes_metadata = axes_metadata.into_iter().map(AxesMetadata::from).collect();
578            figure.set_active_axes_index(figure.active_axes_index);
579        } else {
580            figure.title = self.metadata.title;
581            figure.x_label = self.metadata.x_label;
582            figure.y_label = self.metadata.y_label;
583            figure.legend_enabled = self.metadata.legend_enabled;
584        }
585        figure.name = self.metadata.name;
586        figure.number_title = self.metadata.number_title;
587        figure.visible = self.metadata.visible;
588        figure.sg_title = self.metadata.sg_title;
589        figure.sg_title_style = self
590            .metadata
591            .sg_title_style
592            .map(TextStyle::from)
593            .unwrap_or_default();
594        figure.grid_enabled = self.metadata.grid_enabled;
595        figure.minor_grid_enabled = self.metadata.minor_grid_enabled;
596        figure.z_limits = self.metadata.z_limits.map(|[lo, hi]| (lo, hi));
597        figure.colorbar_enabled = self.metadata.colorbar_enabled;
598        figure.axis_equal = self.metadata.axis_equal;
599        figure.background_color = rgba_to_vec4(self.metadata.background_rgba);
600
601        for plot in self.plots {
602            plot.apply_to_figure(&mut figure)?;
603        }
604
605        Ok(figure)
606    }
607
608    pub fn into_geometry_scene(
609        self,
610        scene_id: impl Into<String>,
611        revision: u64,
612    ) -> Result<crate::GeometryScene, String> {
613        self.validate_schema_version()?;
614        let scene_id = scene_id.into();
615        let mut chunks = Vec::new();
616        for (plot_index, plot) in self.plots.into_iter().enumerate() {
617            append_geometry_scene_chunks(&scene_id, plot_index, plot, &mut chunks)?;
618        }
619        if chunks.is_empty() {
620            return Err("figure scene does not contain renderable mesh plots".to_string());
621        }
622        let mut scene = crate::GeometryScene::new(scene_id, revision, chunks).with_title(
623            self.metadata
624                .title
625                .unwrap_or_else(|| "Geometry Preview".to_string()),
626        );
627        scene.show_grid = self.metadata.grid_enabled;
628        scene.axis_equal = self.metadata.axis_equal;
629        Ok(scene)
630    }
631
632    fn validate_schema_version(&self) -> Result<(), String> {
633        if self.schema_version == 0 || self.schema_version > FigureScene::SCHEMA_VERSION {
634            return Err(format!(
635                "unsupported figure scene schema version {} (supported 1..={})",
636                self.schema_version,
637                FigureScene::SCHEMA_VERSION
638            ));
639        }
640        if self.schema_version < 2
641            && self
642                .plots
643                .iter()
644                .any(|plot| matches!(plot, ScenePlot::Patch { .. }))
645        {
646            return Err(format!(
647                "patch plots require figure scene schema version {}",
648                2
649            ));
650        }
651        if self.schema_version < 3
652            && self
653                .plots
654                .iter()
655                .any(|plot| matches!(plot, ScenePlot::Mesh { .. }))
656        {
657            return Err(format!(
658                "mesh plots require figure scene schema version {}",
659                3
660            ));
661        }
662        Ok(())
663    }
664}
665
666fn append_geometry_scene_chunks(
667    scene_id: &str,
668    plot_index: usize,
669    plot: ScenePlot,
670    chunks: &mut Vec<crate::GeometrySceneChunk>,
671) -> Result<(), String> {
672    let ScenePlot::Mesh {
673        vertices,
674        triangles,
675        mesh_id,
676        face_color_rgba,
677        edge_color_rgba,
678        face_alpha,
679        edge_alpha,
680        edge_width,
681        edge_mode,
682        feature_edge_groups,
683        vertex_colors_rgba,
684        triangle_colors_rgba,
685        axes_index: _,
686        label,
687        regions,
688        highlighted_region_id,
689        highlight_color_rgba,
690        scalar_field,
691        vector_field,
692        deformation,
693        visible,
694    } = plot
695    else {
696        return Ok(());
697    };
698
699    if !visible {
700        return Ok(());
701    }
702
703    let region_metadata = regions
704        .iter()
705        .cloned()
706        .map(crate::geometry_scene::GeometrySceneRegion::from)
707        .collect::<Vec<_>>();
708    let mesh_id_for_chunk = mesh_id
709        .clone()
710        .unwrap_or_else(|| format!("mesh_{}", plot_index + 1));
711    let mut mesh = MeshPlot::new(vertices.into_iter().map(xyz_to_vec3).collect(), triangles)?;
712    mesh.set_mesh_id(mesh_id.clone());
713    mesh.set_face_color(rgba_to_vec4(face_color_rgba));
714    mesh.set_edge_color(rgba_to_vec4(edge_color_rgba));
715    mesh.set_face_alpha(face_alpha);
716    mesh.set_edge_alpha(edge_alpha);
717    mesh.set_edge_width(edge_width);
718    mesh.set_edge_mode(parse_mesh_edge_mode(&edge_mode));
719    if !feature_edge_groups.is_empty() {
720        mesh.set_feature_edge_groups(Some(feature_edge_groups))?;
721    }
722    if !vertex_colors_rgba.is_empty() {
723        mesh.set_vertex_colors(Some(
724            vertex_colors_rgba.into_iter().map(rgba_to_vec4).collect(),
725        ))?;
726    }
727    if !triangle_colors_rgba.is_empty() {
728        mesh.set_triangle_colors(Some(
729            triangle_colors_rgba.into_iter().map(rgba_to_vec4).collect(),
730        ))?;
731    }
732    mesh.set_label(label.clone());
733    mesh.set_regions(regions.into_iter().map(Into::into).collect());
734    mesh.set_highlighted_region_id(highlighted_region_id);
735    if let Some(color) = highlight_color_rgba {
736        mesh.set_highlight_color(rgba_to_vec4(color));
737    }
738    if let Some(field) = scalar_field {
739        mesh.set_scalar_field(Some((*field).try_into()?))?;
740    }
741    if let Some(field) = vector_field {
742        mesh.set_vector_field(Some((*field).try_into()?))?;
743    }
744    if let Some(field) = deformation {
745        mesh.set_deformation(Some((*field).into()))?;
746    }
747
748    let face_render_data = mesh.render_data();
749    chunks.push(
750        crate::GeometrySceneChunk::from_render_data(
751            format!("{scene_id}:{mesh_id_for_chunk}:faces:{plot_index}"),
752            face_render_data,
753        )
754        .with_mesh_id(mesh_id_for_chunk.clone())
755        .with_label(label.clone().unwrap_or_else(|| mesh_id_for_chunk.clone()))
756        .with_regions(region_metadata),
757    );
758
759    if let Some(edge_render_data) = mesh.edge_render_data() {
760        chunks.push(
761            crate::GeometrySceneChunk::from_render_data(
762                format!("{scene_id}:{mesh_id_for_chunk}:edges:{plot_index}"),
763                edge_render_data,
764            )
765            .with_mesh_id(mesh_id_for_chunk.clone())
766            .with_label(format!(
767                "{} edges",
768                label.clone().unwrap_or_else(|| mesh_id_for_chunk.clone())
769            )),
770        );
771    }
772
773    if let Some(vector_render_data) = mesh.vector_render_data() {
774        chunks.push(
775            crate::GeometrySceneChunk::from_render_data(
776                format!("{scene_id}:{mesh_id_for_chunk}:vectors:{plot_index}"),
777                vector_render_data,
778            )
779            .with_mesh_id(mesh_id_for_chunk.clone())
780            .with_label(format!(
781                "{} vectors",
782                label.unwrap_or_else(|| mesh_id_for_chunk.clone())
783            )),
784        );
785    }
786
787    Ok(())
788}
789
790fn scene_chunk_to_mesh_plot(
791    chunk: &crate::geometry_scene::GeometrySceneChunk,
792) -> Option<ScenePlot> {
793    if chunk.render_data.pipeline_type != crate::core::PipelineType::Triangles {
794        return None;
795    }
796    let indices = chunk.indices.as_ref()?;
797    if indices.len() < 3 {
798        return None;
799    }
800    let triangles = indices
801        .chunks_exact(3)
802        .map(|item| [item[0], item[1], item[2]])
803        .collect::<Vec<_>>();
804    if triangles.is_empty() {
805        return None;
806    }
807    let vertices = chunk
808        .vertices
809        .iter()
810        .map(|vertex| vertex.position)
811        .collect::<Vec<_>>();
812    Some(ScenePlot::Mesh {
813        vertices,
814        triangles,
815        mesh_id: chunk.mesh_id.clone(),
816        face_color_rgba: chunk.material.albedo.to_array(),
817        edge_color_rgba: [0.08, 0.10, 0.13, 1.0],
818        face_alpha: chunk.material.albedo.w,
819        edge_alpha: 0.0,
820        edge_width: 0.0,
821        edge_mode: "none".to_string(),
822        feature_edge_groups: Vec::new(),
823        vertex_colors_rgba: Vec::new(),
824        triangle_colors_rgba: Vec::new(),
825        axes_index: 0,
826        label: chunk.label.clone(),
827        regions: chunk.regions.iter().map(Into::into).collect(),
828        highlighted_region_id: None,
829        highlight_color_rgba: Some([0.98, 0.78, 0.22, 1.0]),
830        scalar_field: None,
831        vector_field: None,
832        deformation: None,
833        visible: chunk.visible,
834    })
835}
836
837fn figure_axis_index(figure: &Figure, plot_index: usize) -> u32 {
838    figure
839        .plot_axes_indices()
840        .get(plot_index)
841        .copied()
842        .unwrap_or(0) as u32
843}
844
845/// Layout metadata describing subplot arrangement.
846#[derive(Debug, Clone, Serialize, Deserialize)]
847#[serde(rename_all = "camelCase")]
848pub struct FigureLayout {
849    pub axes_rows: u32,
850    pub axes_cols: u32,
851    pub axes_indices: Vec<u32>,
852}
853
854/// Figure-level metadata (title, labels, theming).
855#[derive(Debug, Clone, Serialize, Deserialize)]
856#[serde(rename_all = "camelCase")]
857pub struct FigureMetadata {
858    #[serde(skip_serializing_if = "Option::is_none")]
859    pub name: Option<String>,
860    #[serde(default = "default_true", skip_serializing_if = "is_true")]
861    pub number_title: bool,
862    #[serde(default = "default_true", skip_serializing_if = "is_true")]
863    pub visible: bool,
864    #[serde(skip_serializing_if = "Option::is_none")]
865    pub title: Option<String>,
866    #[serde(skip_serializing_if = "Option::is_none")]
867    pub sg_title: Option<String>,
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub sg_title_style: Option<SerializedTextStyle>,
870    #[serde(skip_serializing_if = "Option::is_none")]
871    pub x_label: Option<String>,
872    #[serde(skip_serializing_if = "Option::is_none")]
873    pub y_label: Option<String>,
874    pub grid_enabled: bool,
875    #[serde(default)]
876    pub minor_grid_enabled: bool,
877    pub legend_enabled: bool,
878    pub colorbar_enabled: bool,
879    pub axis_equal: bool,
880    pub background_rgba: [f32; 4],
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub colormap: Option<String>,
883    #[serde(skip_serializing_if = "Option::is_none")]
884    pub color_limits: Option<[f64; 2]>,
885    #[serde(skip_serializing_if = "Option::is_none")]
886    pub z_limits: Option<[f64; 2]>,
887    pub legend_entries: Vec<FigureLegendEntry>,
888    #[serde(default)]
889    pub active_axes_index: u32,
890    #[serde(skip_serializing_if = "Option::is_none")]
891    pub axes_metadata: Option<Vec<SerializedAxesMetadata>>,
892}
893
894impl FigureMetadata {
895    fn from_figure(figure: &Figure) -> Self {
896        let legend_entries = figure
897            .legend_entries()
898            .into_iter()
899            .map(FigureLegendEntry::from)
900            .collect();
901
902        Self {
903            name: figure.name.clone(),
904            number_title: figure.number_title,
905            visible: figure.visible,
906            title: figure.title.clone(),
907            sg_title: figure.sg_title.clone(),
908            sg_title_style: figure
909                .sg_title
910                .as_ref()
911                .map(|_| figure.sg_title_style.clone().into()),
912            x_label: figure.x_label.clone(),
913            y_label: figure.y_label.clone(),
914            grid_enabled: figure.grid_enabled,
915            minor_grid_enabled: figure.minor_grid_enabled,
916            legend_enabled: figure.legend_enabled,
917            colorbar_enabled: figure.colorbar_enabled,
918            axis_equal: figure.axis_equal,
919            background_rgba: vec4_to_rgba(figure.background_color),
920            colormap: Some(format!("{:?}", figure.colormap)),
921            color_limits: figure.color_limits.map(|(lo, hi)| [lo, hi]),
922            z_limits: figure.z_limits.map(|(lo, hi)| [lo, hi]),
923            legend_entries,
924            active_axes_index: figure.active_axes_index as u32,
925            axes_metadata: Some(
926                figure
927                    .axes_metadata
928                    .iter()
929                    .cloned()
930                    .map(SerializedAxesMetadata::from)
931                    .collect(),
932            ),
933        }
934    }
935}
936
937fn default_true() -> bool {
938    true
939}
940
941fn is_true(value: &bool) -> bool {
942    *value
943}
944
945fn is_false(value: &bool) -> bool {
946    !*value
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize)]
950#[serde(rename_all = "camelCase")]
951pub struct SerializedTextStyle {
952    #[serde(skip_serializing_if = "Option::is_none")]
953    pub color_rgba: Option<[f32; 4]>,
954    #[serde(skip_serializing_if = "Option::is_none")]
955    pub font_size: Option<f32>,
956    #[serde(skip_serializing_if = "Option::is_none")]
957    pub font_weight: Option<String>,
958    #[serde(skip_serializing_if = "Option::is_none")]
959    pub font_angle: Option<String>,
960    #[serde(skip_serializing_if = "Option::is_none")]
961    pub interpreter: Option<String>,
962    pub visible: bool,
963}
964
965impl Default for SerializedTextStyle {
966    fn default() -> Self {
967        TextStyle::default().into()
968    }
969}
970
971impl From<TextStyle> for SerializedTextStyle {
972    fn from(value: TextStyle) -> Self {
973        Self {
974            color_rgba: value.color.map(vec4_to_rgba),
975            font_size: value.font_size,
976            font_weight: value.font_weight,
977            font_angle: value.font_angle,
978            interpreter: value.interpreter,
979            visible: value.visible,
980        }
981    }
982}
983
984impl From<SerializedTextStyle> for TextStyle {
985    fn from(value: SerializedTextStyle) -> Self {
986        Self {
987            color: value.color_rgba.map(rgba_to_vec4),
988            font_size: value.font_size,
989            font_weight: value.font_weight,
990            font_angle: value.font_angle,
991            interpreter: value.interpreter,
992            visible: value.visible,
993        }
994    }
995}
996
997#[derive(Debug, Clone, Serialize, Deserialize)]
998#[serde(rename_all = "camelCase")]
999pub struct SerializedLegendStyle {
1000    #[serde(skip_serializing_if = "Option::is_none")]
1001    pub location: Option<String>,
1002    pub visible: bool,
1003    #[serde(skip_serializing_if = "Option::is_none")]
1004    pub font_size: Option<f32>,
1005    #[serde(skip_serializing_if = "Option::is_none")]
1006    pub font_weight: Option<String>,
1007    #[serde(skip_serializing_if = "Option::is_none")]
1008    pub font_angle: Option<String>,
1009    #[serde(skip_serializing_if = "Option::is_none")]
1010    pub interpreter: Option<String>,
1011    #[serde(skip_serializing_if = "Option::is_none")]
1012    pub box_visible: Option<bool>,
1013    #[serde(skip_serializing_if = "Option::is_none")]
1014    pub orientation: Option<String>,
1015    #[serde(skip_serializing_if = "Option::is_none")]
1016    pub text_color_rgba: Option<[f32; 4]>,
1017}
1018
1019impl From<LegendStyle> for SerializedLegendStyle {
1020    fn from(value: LegendStyle) -> Self {
1021        Self {
1022            location: value.location,
1023            visible: value.visible,
1024            font_size: value.font_size,
1025            font_weight: value.font_weight,
1026            font_angle: value.font_angle,
1027            interpreter: value.interpreter,
1028            box_visible: value.box_visible,
1029            orientation: value.orientation,
1030            text_color_rgba: value.text_color.map(vec4_to_rgba),
1031        }
1032    }
1033}
1034
1035impl From<SerializedLegendStyle> for LegendStyle {
1036    fn from(value: SerializedLegendStyle) -> Self {
1037        Self {
1038            location: value.location,
1039            visible: value.visible,
1040            font_size: value.font_size,
1041            font_weight: value.font_weight,
1042            font_angle: value.font_angle,
1043            interpreter: value.interpreter,
1044            box_visible: value.box_visible,
1045            orientation: value.orientation,
1046            text_color: value.text_color_rgba.map(rgba_to_vec4),
1047        }
1048    }
1049}
1050
1051#[derive(Debug, Clone, Serialize, Deserialize)]
1052#[serde(rename_all = "camelCase")]
1053pub struct SerializedAxesMetadata {
1054    #[serde(default, skip_serializing_if = "is_cartesian_axes_kind")]
1055    pub axes_kind: SerializedAxesKind,
1056    #[serde(skip_serializing_if = "Option::is_none")]
1057    pub title: Option<String>,
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub x_label: Option<String>,
1060    #[serde(skip_serializing_if = "Option::is_none")]
1061    pub y_label: Option<String>,
1062    #[serde(skip_serializing_if = "Option::is_none")]
1063    pub z_label: Option<String>,
1064    #[serde(default, skip_serializing_if = "Option::is_none")]
1065    pub x_tick_labels: Option<Vec<String>>,
1066    #[serde(default, skip_serializing_if = "Option::is_none")]
1067    pub y_tick_labels: Option<Vec<String>>,
1068    #[serde(skip_serializing_if = "Option::is_none")]
1069    pub x_limits: Option<[f64; 2]>,
1070    #[serde(skip_serializing_if = "Option::is_none")]
1071    pub y_limits: Option<[f64; 2]>,
1072    #[serde(skip_serializing_if = "Option::is_none")]
1073    pub z_limits: Option<[f64; 2]>,
1074    #[serde(default)]
1075    pub x_log: bool,
1076    #[serde(default)]
1077    pub y_log: bool,
1078    #[serde(skip_serializing_if = "Option::is_none")]
1079    pub view_azimuth_deg: Option<f32>,
1080    #[serde(skip_serializing_if = "Option::is_none")]
1081    pub view_elevation_deg: Option<f32>,
1082    #[serde(default)]
1083    pub grid_enabled: bool,
1084    #[serde(default)]
1085    pub minor_grid_enabled: bool,
1086    #[serde(default, skip_serializing_if = "is_false")]
1087    pub minor_grid_explicit: bool,
1088    #[serde(default)]
1089    pub box_enabled: bool,
1090    #[serde(default)]
1091    pub axis_equal: bool,
1092    pub legend_enabled: bool,
1093    #[serde(default)]
1094    pub colorbar_enabled: bool,
1095    pub colormap: String,
1096    #[serde(skip_serializing_if = "Option::is_none")]
1097    pub color_limits: Option<[f64; 2]>,
1098    #[serde(default)]
1099    pub axes_style: SerializedTextStyle,
1100    pub title_style: SerializedTextStyle,
1101    pub x_label_style: SerializedTextStyle,
1102    pub y_label_style: SerializedTextStyle,
1103    pub z_label_style: SerializedTextStyle,
1104    pub legend_style: SerializedLegendStyle,
1105    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1106    pub world_text_annotations: Vec<SerializedTextAnnotation>,
1107}
1108
1109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1110#[serde(rename_all = "camelCase")]
1111pub enum SerializedAxesKind {
1112    #[default]
1113    Cartesian,
1114    Polar,
1115}
1116
1117fn is_cartesian_axes_kind(value: &SerializedAxesKind) -> bool {
1118    *value == SerializedAxesKind::Cartesian
1119}
1120
1121impl From<AxesKind> for SerializedAxesKind {
1122    fn from(value: AxesKind) -> Self {
1123        match value {
1124            AxesKind::Cartesian => Self::Cartesian,
1125            AxesKind::Polar => Self::Polar,
1126        }
1127    }
1128}
1129
1130impl From<SerializedAxesKind> for AxesKind {
1131    fn from(value: SerializedAxesKind) -> Self {
1132        match value {
1133            SerializedAxesKind::Cartesian => Self::Cartesian,
1134            SerializedAxesKind::Polar => Self::Polar,
1135        }
1136    }
1137}
1138
1139#[derive(Debug, Clone, Serialize, Deserialize)]
1140#[serde(rename_all = "camelCase")]
1141pub struct SerializedTextAnnotation {
1142    pub position: [f32; 3],
1143    pub text: String,
1144    pub style: SerializedTextStyle,
1145}
1146
1147#[derive(Debug, Clone, Serialize, Deserialize)]
1148#[serde(rename_all = "camelCase")]
1149pub struct SerializedMeshRegion {
1150    pub region_id: String,
1151    #[serde(default, skip_serializing_if = "Option::is_none")]
1152    pub label: Option<String>,
1153    #[serde(default, skip_serializing_if = "Option::is_none")]
1154    pub tag: Option<String>,
1155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1156    pub triangle_ranges: Vec<SerializedMeshTriangleRange>,
1157}
1158
1159#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1160#[serde(rename_all = "camelCase")]
1161pub struct SerializedMeshTriangleRange {
1162    pub start: u32,
1163    pub count: u32,
1164}
1165
1166#[derive(Debug, Clone, Serialize, Deserialize)]
1167#[serde(rename_all = "camelCase")]
1168pub struct SerializedMeshScalarField {
1169    pub field_id: String,
1170    #[serde(default, skip_serializing_if = "Option::is_none")]
1171    pub label: Option<String>,
1172    pub location: String,
1173    #[serde(deserialize_with = "deserialize_vec_f32_lossy")]
1174    pub values: Vec<f32>,
1175    #[serde(default, skip_serializing_if = "Option::is_none")]
1176    pub color_limits: Option<[f32; 2]>,
1177    pub colormap: String,
1178    pub alpha: f32,
1179}
1180
1181#[derive(Debug, Clone, Serialize, Deserialize)]
1182#[serde(rename_all = "camelCase")]
1183pub struct SerializedMeshVectorField {
1184    pub field_id: String,
1185    #[serde(default, skip_serializing_if = "Option::is_none")]
1186    pub label: Option<String>,
1187    pub location: String,
1188    #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
1189    pub vectors: Vec<[f32; 3]>,
1190    pub scale: f32,
1191    pub stride: usize,
1192    pub color_rgba: [f32; 4],
1193}
1194
1195#[derive(Debug, Clone, Serialize, Deserialize)]
1196#[serde(rename_all = "camelCase")]
1197pub struct SerializedMeshDeformation {
1198    pub field_id: String,
1199    #[serde(default, skip_serializing_if = "Option::is_none")]
1200    pub label: Option<String>,
1201    #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
1202    pub displacements: Vec<[f32; 3]>,
1203    pub scale: f32,
1204}
1205
1206impl From<AxesMetadata> for SerializedAxesMetadata {
1207    fn from(value: AxesMetadata) -> Self {
1208        Self {
1209            axes_kind: value.axes_kind.into(),
1210            title: value.title,
1211            x_label: value.x_label,
1212            y_label: value.y_label,
1213            z_label: value.z_label,
1214            x_tick_labels: value.x_tick_labels,
1215            y_tick_labels: value.y_tick_labels,
1216            x_limits: value.x_limits.map(|(a, b)| [a, b]),
1217            y_limits: value.y_limits.map(|(a, b)| [a, b]),
1218            z_limits: value.z_limits.map(|(a, b)| [a, b]),
1219            x_log: value.x_log,
1220            y_log: value.y_log,
1221            view_azimuth_deg: value.view_azimuth_deg,
1222            view_elevation_deg: value.view_elevation_deg,
1223            grid_enabled: value.grid_enabled,
1224            minor_grid_enabled: value.minor_grid_enabled,
1225            minor_grid_explicit: value.minor_grid_explicit,
1226            box_enabled: value.box_enabled,
1227            axis_equal: value.axis_equal,
1228            legend_enabled: value.legend_enabled,
1229            colorbar_enabled: value.colorbar_enabled,
1230            colormap: format!("{:?}", value.colormap),
1231            color_limits: value.color_limits.map(|(a, b)| [a, b]),
1232            axes_style: value.axes_style.into(),
1233            title_style: value.title_style.into(),
1234            x_label_style: value.x_label_style.into(),
1235            y_label_style: value.y_label_style.into(),
1236            z_label_style: value.z_label_style.into(),
1237            legend_style: value.legend_style.into(),
1238            world_text_annotations: value
1239                .world_text_annotations
1240                .into_iter()
1241                .map(Into::into)
1242                .collect(),
1243        }
1244    }
1245}
1246
1247impl From<SerializedAxesMetadata> for AxesMetadata {
1248    fn from(value: SerializedAxesMetadata) -> Self {
1249        Self {
1250            axes_kind: value.axes_kind.into(),
1251            title: value.title,
1252            x_label: value.x_label,
1253            y_label: value.y_label,
1254            z_label: value.z_label,
1255            x_tick_labels: value.x_tick_labels,
1256            y_tick_labels: value.y_tick_labels,
1257            x_limits: value.x_limits.map(|[a, b]| (a, b)),
1258            y_limits: value.y_limits.map(|[a, b]| (a, b)),
1259            z_limits: value.z_limits.map(|[a, b]| (a, b)),
1260            x_log: value.x_log,
1261            y_log: value.y_log,
1262            view_azimuth_deg: value.view_azimuth_deg,
1263            view_elevation_deg: value.view_elevation_deg,
1264            view_revision: 0,
1265            grid_enabled: value.grid_enabled,
1266            minor_grid_enabled: value.minor_grid_enabled,
1267            minor_grid_explicit: value.minor_grid_explicit || value.minor_grid_enabled,
1268            box_enabled: value.box_enabled,
1269            axis_equal: value.axis_equal,
1270            legend_enabled: value.legend_enabled,
1271            colorbar_enabled: value.colorbar_enabled,
1272            colormap: parse_colormap_name(&value.colormap),
1273            color_limits: value.color_limits.map(|[a, b]| (a, b)),
1274            axes_style: value.axes_style.into(),
1275            title_style: value.title_style.into(),
1276            x_label_style: value.x_label_style.into(),
1277            y_label_style: value.y_label_style.into(),
1278            z_label_style: value.z_label_style.into(),
1279            legend_style: value.legend_style.into(),
1280            world_text_annotations: value
1281                .world_text_annotations
1282                .into_iter()
1283                .map(Into::into)
1284                .collect(),
1285        }
1286    }
1287}
1288
1289impl From<crate::plots::figure::TextAnnotation> for SerializedTextAnnotation {
1290    fn from(value: crate::plots::figure::TextAnnotation) -> Self {
1291        Self {
1292            position: value.position.to_array(),
1293            text: value.text,
1294            style: value.style.into(),
1295        }
1296    }
1297}
1298
1299impl From<SerializedTextAnnotation> for crate::plots::figure::TextAnnotation {
1300    fn from(value: SerializedTextAnnotation) -> Self {
1301        Self {
1302            position: glam::Vec3::from_array(value.position),
1303            text: value.text,
1304            style: value.style.into(),
1305        }
1306    }
1307}
1308
1309impl From<&MeshRegion> for SerializedMeshRegion {
1310    fn from(value: &MeshRegion) -> Self {
1311        Self {
1312            region_id: value.region_id.clone(),
1313            label: value.label.clone(),
1314            tag: value.tag.clone(),
1315            triangle_ranges: value
1316                .triangle_ranges
1317                .iter()
1318                .copied()
1319                .map(Into::into)
1320                .collect(),
1321        }
1322    }
1323}
1324
1325impl From<&crate::geometry_scene::GeometrySceneRegion> for SerializedMeshRegion {
1326    fn from(value: &crate::geometry_scene::GeometrySceneRegion) -> Self {
1327        Self {
1328            region_id: value.region_id.clone(),
1329            label: value.label.clone(),
1330            tag: value.tag.clone(),
1331            triangle_ranges: value
1332                .triangle_ranges
1333                .iter()
1334                .copied()
1335                .map(Into::into)
1336                .collect(),
1337        }
1338    }
1339}
1340
1341impl From<SerializedMeshRegion> for MeshRegion {
1342    fn from(value: SerializedMeshRegion) -> Self {
1343        MeshRegion {
1344            region_id: value.region_id,
1345            label: value.label,
1346            tag: value.tag,
1347            triangle_ranges: value.triangle_ranges.into_iter().map(Into::into).collect(),
1348        }
1349    }
1350}
1351
1352impl From<SerializedMeshRegion> for crate::geometry_scene::GeometrySceneRegion {
1353    fn from(value: SerializedMeshRegion) -> Self {
1354        crate::geometry_scene::GeometrySceneRegion::new(
1355            value.region_id,
1356            value.label,
1357            value.tag,
1358            value.triangle_ranges.into_iter().map(Into::into).collect(),
1359        )
1360    }
1361}
1362
1363impl From<MeshTriangleRange> for SerializedMeshTriangleRange {
1364    fn from(value: MeshTriangleRange) -> Self {
1365        Self {
1366            start: value.start,
1367            count: value.count,
1368        }
1369    }
1370}
1371
1372impl From<crate::geometry_scene::GeometrySceneTriangleRange> for SerializedMeshTriangleRange {
1373    fn from(value: crate::geometry_scene::GeometrySceneTriangleRange) -> Self {
1374        Self {
1375            start: value.start,
1376            count: value.count,
1377        }
1378    }
1379}
1380
1381impl From<SerializedMeshTriangleRange> for MeshTriangleRange {
1382    fn from(value: SerializedMeshTriangleRange) -> Self {
1383        Self::new(value.start, value.count)
1384    }
1385}
1386
1387impl From<SerializedMeshTriangleRange> for crate::geometry_scene::GeometrySceneTriangleRange {
1388    fn from(value: SerializedMeshTriangleRange) -> Self {
1389        Self::new(value.start, value.count)
1390    }
1391}
1392
1393impl From<&MeshScalarField> for SerializedMeshScalarField {
1394    fn from(value: &MeshScalarField) -> Self {
1395        Self {
1396            field_id: value.field_id.clone(),
1397            label: value.label.clone(),
1398            location: value.location.as_str().to_string(),
1399            values: value.values.clone(),
1400            color_limits: value.color_limits,
1401            colormap: value.colormap.clone(),
1402            alpha: value.alpha,
1403        }
1404    }
1405}
1406
1407impl TryFrom<SerializedMeshScalarField> for MeshScalarField {
1408    type Error = String;
1409
1410    fn try_from(value: SerializedMeshScalarField) -> Result<Self, Self::Error> {
1411        Ok(Self {
1412            field_id: value.field_id,
1413            label: value.label,
1414            location: MeshFieldLocation::parse(&value.location).ok_or_else(|| {
1415                format!("unknown mesh scalar field location '{}'", value.location)
1416            })?,
1417            values: value.values,
1418            color_limits: value.color_limits,
1419            colormap: value.colormap,
1420            alpha: value.alpha,
1421        })
1422    }
1423}
1424
1425impl From<&MeshVectorField> for SerializedMeshVectorField {
1426    fn from(value: &MeshVectorField) -> Self {
1427        Self {
1428            field_id: value.field_id.clone(),
1429            label: value.label.clone(),
1430            location: value.location.as_str().to_string(),
1431            vectors: value
1432                .vectors
1433                .iter()
1434                .map(|vector| vector.to_array())
1435                .collect(),
1436            scale: value.scale,
1437            stride: value.stride,
1438            color_rgba: vec4_to_rgba(value.color),
1439        }
1440    }
1441}
1442
1443impl TryFrom<SerializedMeshVectorField> for MeshVectorField {
1444    type Error = String;
1445
1446    fn try_from(value: SerializedMeshVectorField) -> Result<Self, Self::Error> {
1447        Ok(Self {
1448            field_id: value.field_id,
1449            label: value.label,
1450            location: MeshFieldLocation::parse(&value.location).ok_or_else(|| {
1451                format!("unknown mesh vector field location '{}'", value.location)
1452            })?,
1453            vectors: value.vectors.into_iter().map(Vec3::from_array).collect(),
1454            scale: value.scale,
1455            stride: value.stride,
1456            color: rgba_to_vec4(value.color_rgba),
1457        })
1458    }
1459}
1460
1461impl From<&MeshDeformation> for SerializedMeshDeformation {
1462    fn from(value: &MeshDeformation) -> Self {
1463        Self {
1464            field_id: value.field_id.clone(),
1465            label: value.label.clone(),
1466            displacements: value
1467                .displacements
1468                .iter()
1469                .map(|displacement| displacement.to_array())
1470                .collect(),
1471            scale: value.scale,
1472        }
1473    }
1474}
1475
1476impl From<SerializedMeshDeformation> for MeshDeformation {
1477    fn from(value: SerializedMeshDeformation) -> Self {
1478        Self {
1479            field_id: value.field_id,
1480            label: value.label,
1481            displacements: value
1482                .displacements
1483                .into_iter()
1484                .map(Vec3::from_array)
1485                .collect(),
1486            scale: value.scale,
1487        }
1488    }
1489}
1490
1491/// Descriptor for a single plot element within the figure.
1492#[derive(Debug, Clone, Serialize, Deserialize)]
1493#[serde(rename_all = "camelCase")]
1494pub struct PlotDescriptor {
1495    pub kind: PlotKind,
1496    #[serde(skip_serializing_if = "Option::is_none")]
1497    pub label: Option<String>,
1498    pub axes_index: u32,
1499    pub color_rgba: [f32; 4],
1500    pub visible: bool,
1501}
1502
1503impl PlotDescriptor {
1504    fn from_plot(plot: &PlotElement, axes_index: u32) -> Self {
1505        Self {
1506            kind: PlotKind::from(plot.plot_type()),
1507            label: plot.label(),
1508            axes_index,
1509            color_rgba: vec4_to_rgba(plot.color()),
1510            visible: plot.is_visible(),
1511        }
1512    }
1513}
1514
1515fn validate_required_equal_lengths(
1516    kind: &str,
1517    fields: &[(&str, usize)],
1518) -> Result<(), SceneExportError> {
1519    let Some((first_name, first_len)) = fields.first().copied() else {
1520        return Ok(());
1521    };
1522    if first_len == 0 {
1523        return Err(SceneExportError::unexportable(format!(
1524            "{kind} is missing required {first_name} values"
1525        )));
1526    }
1527    for (name, len) in fields.iter().copied().skip(1) {
1528        if len == 0 {
1529            return Err(SceneExportError::unexportable(format!(
1530                "{kind} is missing required {name} values"
1531            )));
1532        }
1533        if len != first_len {
1534            return Err(SceneExportError::unexportable(format!(
1535                "{kind} length mismatch: {name} has {len} values, expected {first_len}"
1536            )));
1537        }
1538    }
1539    Ok(())
1540}
1541
1542fn validate_surface_grid<T>(
1543    kind: &str,
1544    x: &[f64],
1545    y: &[f64],
1546    grid: &[Vec<T>],
1547) -> Result<(), SceneExportError> {
1548    if x.is_empty() {
1549        return Err(SceneExportError::unexportable(format!(
1550            "{kind} is missing required x values"
1551        )));
1552    }
1553    if y.is_empty() {
1554        return Err(SceneExportError::unexportable(format!(
1555            "{kind} is missing required y values"
1556        )));
1557    }
1558    if grid.is_empty() {
1559        return Err(SceneExportError::unexportable(format!(
1560            "{kind} is missing required grid rows"
1561        )));
1562    }
1563    if grid.len() != x.len() {
1564        return Err(SceneExportError::unexportable(format!(
1565            "{kind} row count ({}) must match x length ({})",
1566            grid.len(),
1567            x.len()
1568        )));
1569    }
1570    for (row_idx, row) in grid.iter().enumerate() {
1571        if row.len() != y.len() {
1572            return Err(SceneExportError::unexportable(format!(
1573                "{kind} row {row_idx} length ({}) must match y length ({})",
1574                row.len(),
1575                y.len()
1576            )));
1577        }
1578    }
1579    Ok(())
1580}
1581
1582impl ScenePlot {
1583    async fn from_plot_for_export(
1584        plot: &PlotElement,
1585        axes_index: u32,
1586    ) -> Result<Self, SceneExportError> {
1587        let scene_plot = match plot {
1588            PlotElement::Line(line) => {
1589                let (x, y) = line
1590                    .export_scene_xy_data()
1591                    .await
1592                    .map_err(SceneExportError::readback)?;
1593                if x.is_empty() && y.is_empty() {
1594                    return Err(SceneExportError::unexportable(
1595                        "line plot has no exportable scene data",
1596                    ));
1597                }
1598                Self::Line {
1599                    x,
1600                    y,
1601                    color_rgba: vec4_to_rgba(line.color),
1602                    line_width: line.line_width,
1603                    line_style: format!("{:?}", line.line_style),
1604                    axes_index,
1605                    label: line.label.clone(),
1606                    visible: line.visible,
1607                }
1608            }
1609            PlotElement::ReferenceLine(_) => Self::from_plot(plot, axes_index),
1610            PlotElement::Scatter(scatter) => {
1611                let (x, y) = scatter
1612                    .export_scene_xy_data()
1613                    .await
1614                    .map_err(SceneExportError::readback)?;
1615                if x.is_empty() && y.is_empty() {
1616                    return Err(SceneExportError::unexportable(
1617                        "scatter plot has no exportable scene data",
1618                    ));
1619                }
1620                Self::Scatter {
1621                    x,
1622                    y,
1623                    color_rgba: vec4_to_rgba(scatter.color),
1624                    marker_size: scatter.marker_size,
1625                    marker_style: format!("{:?}", scatter.marker_style),
1626                    axes_index,
1627                    label: scatter.label.clone(),
1628                    visible: scatter.visible,
1629                }
1630            }
1631            PlotElement::Bar(bar) => {
1632                let values = bar
1633                    .export_scene_values()
1634                    .await
1635                    .map_err(SceneExportError::readback)?;
1636                if values.is_empty() {
1637                    return Err(SceneExportError::unexportable(
1638                        "bar chart has no exportable scene data",
1639                    ));
1640                }
1641                Self::Bar {
1642                    labels: bar.labels.clone(),
1643                    values,
1644                    histogram_bin_edges: bar.histogram_bin_edges().map(|edges| edges.to_vec()),
1645                    color_rgba: vec4_to_rgba(bar.color),
1646                    outline_color_rgba: bar.outline_color.map(vec4_to_rgba),
1647                    bar_width: bar.bar_width,
1648                    outline_width: bar.outline_width,
1649                    orientation: format!("{:?}", bar.orientation),
1650                    group_index: bar.group_index as u32,
1651                    group_count: bar.group_count as u32,
1652                    stack_offsets: bar.stack_offsets().map(|offsets| offsets.to_vec()),
1653                    axes_index,
1654                    label: bar.label.clone(),
1655                    visible: bar.visible,
1656                }
1657            }
1658            PlotElement::ErrorBar(error) => {
1659                let (x, y, y_neg, y_pos, x_neg, x_pos) = error
1660                    .export_scene_data()
1661                    .await
1662                    .map_err(SceneExportError::readback)?;
1663                if x.is_empty() && y.is_empty() {
1664                    return Err(SceneExportError::unexportable(
1665                        "errorbar plot has no exportable scene data",
1666                    ));
1667                }
1668                Self::ErrorBar {
1669                    x,
1670                    y,
1671                    err_low: y_neg,
1672                    err_high: y_pos,
1673                    x_err_low: x_neg,
1674                    x_err_high: x_pos,
1675                    orientation: format!("{:?}", error.orientation),
1676                    color_rgba: vec4_to_rgba(error.color),
1677                    line_width: error.line_width,
1678                    line_style: format!("{:?}", error.line_style),
1679                    cap_width: error.cap_size,
1680                    marker_style: error.marker.as_ref().map(|m| format!("{:?}", m.kind)),
1681                    marker_size: error.marker.as_ref().map(|m| m.size),
1682                    marker_face_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.face_color)),
1683                    marker_edge_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.edge_color)),
1684                    marker_filled: error.marker.as_ref().map(|m| m.filled),
1685                    axes_index,
1686                    label: error.label.clone(),
1687                    visible: error.visible,
1688                }
1689            }
1690            PlotElement::Stairs(stairs) => {
1691                let (x, y) = stairs
1692                    .export_scene_xy_data()
1693                    .await
1694                    .map_err(SceneExportError::readback)?;
1695                if x.is_empty() && y.is_empty() {
1696                    return Err(SceneExportError::unexportable(
1697                        "stairs plot has no exportable scene data",
1698                    ));
1699                }
1700                Self::Stairs {
1701                    x,
1702                    y,
1703                    color_rgba: vec4_to_rgba(stairs.color),
1704                    line_width: stairs.line_width,
1705                    axes_index,
1706                    label: stairs.label.clone(),
1707                    visible: stairs.visible,
1708                }
1709            }
1710            PlotElement::Stem(stem) => {
1711                let (x, y) = stem
1712                    .export_scene_xy_data()
1713                    .await
1714                    .map_err(SceneExportError::readback)?;
1715                if x.is_empty() && y.is_empty() {
1716                    return Err(SceneExportError::unexportable(
1717                        "stem plot has no exportable scene data",
1718                    ));
1719                }
1720                Self::Stem {
1721                    x,
1722                    y,
1723                    baseline: stem.baseline,
1724                    color_rgba: vec4_to_rgba(stem.color),
1725                    line_width: stem.line_width,
1726                    line_style: format!("{:?}", stem.line_style),
1727                    baseline_color_rgba: vec4_to_rgba(stem.baseline_color),
1728                    baseline_visible: stem.baseline_visible,
1729                    marker_color_rgba: vec4_to_rgba(
1730                        stem.marker
1731                            .as_ref()
1732                            .map(|m| m.face_color)
1733                            .unwrap_or(stem.color),
1734                    ),
1735                    marker_size: stem.marker.as_ref().map(|m| m.size).unwrap_or(0.0),
1736                    marker_filled: stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
1737                    axes_index,
1738                    label: stem.label.clone(),
1739                    visible: stem.visible,
1740                }
1741            }
1742            PlotElement::Area(area) => {
1743                let (x, y) = area
1744                    .export_scene_xy_data()
1745                    .await
1746                    .map_err(SceneExportError::readback)?;
1747                if x.is_empty() && y.is_empty() {
1748                    return Err(SceneExportError::unexportable(
1749                        "area plot has no exportable scene data",
1750                    ));
1751                }
1752                Self::Area {
1753                    x,
1754                    y,
1755                    lower_y: area.lower_y.clone(),
1756                    baseline: area.baseline,
1757                    color_rgba: vec4_to_rgba(area.color),
1758                    axes_index,
1759                    label: area.label.clone(),
1760                    visible: area.visible,
1761                }
1762            }
1763            PlotElement::Quiver(quiver) => {
1764                let (x, y, u, v) = quiver
1765                    .export_scene_vector_data()
1766                    .await
1767                    .map_err(SceneExportError::readback)?;
1768                if x.is_empty() && y.is_empty() && u.is_empty() && v.is_empty() {
1769                    return Err(SceneExportError::unexportable(
1770                        "quiver plot has no exportable scene data",
1771                    ));
1772                }
1773                Self::Quiver {
1774                    x,
1775                    y,
1776                    u,
1777                    v,
1778                    color_rgba: vec4_to_rgba(quiver.color),
1779                    line_width: quiver.line_width,
1780                    scale: quiver.scale,
1781                    head_size: quiver.head_size,
1782                    axes_index,
1783                    label: quiver.label.clone(),
1784                    visible: quiver.visible,
1785                }
1786            }
1787            PlotElement::Surface(surface) => {
1788                let (x, y, z) = surface
1789                    .export_scene_grid_data()
1790                    .await
1791                    .map_err(SceneExportError::readback)?;
1792                if x.is_empty() && y.is_empty() && z.is_empty() {
1793                    return Err(SceneExportError::unexportable(
1794                        "surface plot has no exportable scene data",
1795                    ));
1796                }
1797                let color_grid = surface
1798                    .export_scene_color_grid()
1799                    .await
1800                    .map_err(SceneExportError::readback)?;
1801                Self::Surface {
1802                    x,
1803                    y,
1804                    z,
1805                    colormap: format!("{:?}", surface.colormap),
1806                    shading_mode: format!("{:?}", surface.shading_mode),
1807                    wireframe: surface.wireframe,
1808                    alpha: surface.alpha,
1809                    flatten_z: surface.flatten_z,
1810                    image_mode: surface.image_mode,
1811                    color_grid_rgba: color_grid.as_ref().map(|grid| {
1812                        grid.iter()
1813                            .map(|row| row.iter().map(|color| vec4_to_rgba(*color)).collect())
1814                            .collect()
1815                    }),
1816                    color_limits: surface.color_limits.map(|(lo, hi)| [lo, hi]),
1817                    axes_index,
1818                    label: surface.label.clone(),
1819                    visible: surface.visible,
1820                }
1821            }
1822            PlotElement::Patch(_) | PlotElement::Mesh(_) | PlotElement::Pie(_) => {
1823                Self::from_plot(plot, axes_index)
1824            }
1825            PlotElement::Line3(line) => {
1826                let (x, y, z) = line
1827                    .export_scene_xyz_data()
1828                    .await
1829                    .map_err(SceneExportError::readback)?;
1830                if x.is_empty() && y.is_empty() && z.is_empty() {
1831                    return Err(SceneExportError::unexportable(
1832                        "plot3 line has no exportable scene data",
1833                    ));
1834                }
1835                Self::Line3 {
1836                    x,
1837                    y,
1838                    z,
1839                    color_rgba: vec4_to_rgba(line.color),
1840                    line_width: line.line_width,
1841                    line_style: format!("{:?}", line.line_style),
1842                    axes_index,
1843                    label: line.label.clone(),
1844                    visible: line.visible,
1845                }
1846            }
1847            PlotElement::Scatter3(scatter3) => {
1848                let points = scatter3
1849                    .export_scene_points()
1850                    .await
1851                    .map_err(SceneExportError::readback)?;
1852                if points.is_empty() {
1853                    return Err(SceneExportError::unexportable(
1854                        "scatter3 plot has no exportable scene data",
1855                    ));
1856                }
1857                let colors = scatter3
1858                    .export_scene_colors(points.len())
1859                    .await
1860                    .map_err(SceneExportError::readback)?;
1861                Self::Scatter3 {
1862                    points: points.into_iter().map(vec3_to_xyz).collect(),
1863                    colors_rgba: colors.into_iter().map(vec4_to_rgba).collect(),
1864                    point_size: scatter3.point_size,
1865                    point_sizes: scatter3.point_sizes.clone(),
1866                    axes_index,
1867                    label: scatter3.label.clone(),
1868                    visible: scatter3.visible,
1869                }
1870            }
1871            PlotElement::Contour(contour) => {
1872                let vertices = contour
1873                    .export_scene_vertices()
1874                    .await
1875                    .map_err(SceneExportError::readback)?;
1876                if vertices.is_empty() {
1877                    return Err(SceneExportError::unexportable(
1878                        "contour plot has no exportable scene data",
1879                    ));
1880                }
1881                Self::Contour {
1882                    vertices: vertices.into_iter().map(Into::into).collect(),
1883                    bounds_min: vec3_to_xyz(contour.bounds().min),
1884                    bounds_max: vec3_to_xyz(contour.bounds().max),
1885                    base_z: contour.base_z,
1886                    line_width: contour.line_width,
1887                    axes_index,
1888                    label: contour.label.clone(),
1889                    visible: contour.visible,
1890                    force_3d: contour.force_3d,
1891                }
1892            }
1893            PlotElement::ContourFill(fill) => {
1894                let vertices = fill
1895                    .export_scene_vertices()
1896                    .await
1897                    .map_err(SceneExportError::readback)?;
1898                if vertices.is_empty() {
1899                    return Err(SceneExportError::unexportable(
1900                        "filled contour plot has no exportable scene data",
1901                    ));
1902                }
1903                Self::ContourFill {
1904                    vertices: vertices.into_iter().map(Into::into).collect(),
1905                    bounds_min: vec3_to_xyz(fill.bounds().min),
1906                    bounds_max: vec3_to_xyz(fill.bounds().max),
1907                    axes_index,
1908                    label: fill.label.clone(),
1909                    visible: fill.visible,
1910                }
1911            }
1912        };
1913        scene_plot.validate_exportable()?;
1914        Ok(scene_plot)
1915    }
1916
1917    fn validate_exportable(&self) -> Result<(), SceneExportError> {
1918        match self {
1919            ScenePlot::Line { x, y, .. }
1920            | ScenePlot::Scatter { x, y, .. }
1921            | ScenePlot::Stairs { x, y, .. }
1922            | ScenePlot::Stem { x, y, .. }
1923            | ScenePlot::Area { x, y, .. } => {
1924                validate_required_equal_lengths(
1925                    "plot X/Y scene data",
1926                    &[("x", x.len()), ("y", y.len())],
1927                )?;
1928            }
1929            ScenePlot::ErrorBar {
1930                x,
1931                y,
1932                err_low,
1933                err_high,
1934                x_err_low,
1935                x_err_high,
1936                ..
1937            } => {
1938                validate_required_equal_lengths(
1939                    "errorbar scene data",
1940                    &[
1941                        ("x", x.len()),
1942                        ("y", y.len()),
1943                        ("err_low", err_low.len()),
1944                        ("err_high", err_high.len()),
1945                        ("x_err_low", x_err_low.len()),
1946                        ("x_err_high", x_err_high.len()),
1947                    ],
1948                )?;
1949            }
1950            ScenePlot::Quiver { x, y, u, v, .. } => {
1951                validate_required_equal_lengths(
1952                    "quiver vector field scene data",
1953                    &[
1954                        ("x", x.len()),
1955                        ("y", y.len()),
1956                        ("u", u.len()),
1957                        ("v", v.len()),
1958                    ],
1959                )?;
1960            }
1961            ScenePlot::Bar {
1962                labels,
1963                values,
1964                histogram_bin_edges,
1965                stack_offsets,
1966                ..
1967            } => {
1968                validate_required_equal_lengths(
1969                    "bar value scene data",
1970                    &[("labels", labels.len()), ("values", values.len())],
1971                )?;
1972                if let Some(edges) = histogram_bin_edges {
1973                    if edges.len() != values.len() + 1 {
1974                        return Err(SceneExportError::unexportable(format!(
1975                            "bar histogram bin edge count ({}) must be values length + 1 ({})",
1976                            edges.len(),
1977                            values.len() + 1
1978                        )));
1979                    }
1980                }
1981                if let Some(offsets) = stack_offsets {
1982                    if offsets.len() != values.len() {
1983                        return Err(SceneExportError::unexportable(format!(
1984                            "bar stack offset count ({}) must match value count ({})",
1985                            offsets.len(),
1986                            values.len()
1987                        )));
1988                    }
1989                }
1990            }
1991            ScenePlot::Surface {
1992                x,
1993                y,
1994                z,
1995                color_grid_rgba,
1996                ..
1997            } => {
1998                validate_surface_grid("surface grid scene data", x, y, z)?;
1999                if let Some(color_grid) = color_grid_rgba {
2000                    validate_surface_grid("surface color grid scene data", x, y, color_grid)?;
2001                }
2002            }
2003            ScenePlot::Patch {
2004                vertices, faces, ..
2005            } => {
2006                if vertices.is_empty() || faces.is_empty() {
2007                    return Err(SceneExportError::unexportable(
2008                        "patch plot has no exportable mesh scene data",
2009                    ));
2010                }
2011            }
2012            ScenePlot::Mesh {
2013                vertices,
2014                triangles,
2015                ..
2016            } => {
2017                if vertices.is_empty() || triangles.is_empty() {
2018                    return Err(SceneExportError::unexportable(
2019                        "mesh plot has no exportable mesh scene data",
2020                    ));
2021                }
2022            }
2023            ScenePlot::Line3 { x, y, z, .. } => {
2024                validate_required_equal_lengths(
2025                    "plot3 line scene data",
2026                    &[("x", x.len()), ("y", y.len()), ("z", z.len())],
2027                )?;
2028            }
2029            ScenePlot::Scatter3 {
2030                points,
2031                colors_rgba,
2032                point_sizes,
2033                ..
2034            } => {
2035                if points.is_empty() {
2036                    return Err(SceneExportError::unexportable(
2037                        "scatter3 plot has no exportable point scene data",
2038                    ));
2039                }
2040                if !colors_rgba.is_empty() && colors_rgba.len() != points.len() {
2041                    return Err(SceneExportError::unexportable(format!(
2042                        "scatter3 color count ({}) must match point count ({})",
2043                        colors_rgba.len(),
2044                        points.len()
2045                    )));
2046                }
2047                if let Some(sizes) = point_sizes {
2048                    if sizes.len() != points.len() {
2049                        return Err(SceneExportError::unexportable(format!(
2050                            "scatter3 point size count ({}) must match point count ({})",
2051                            sizes.len(),
2052                            points.len()
2053                        )));
2054                    }
2055                }
2056            }
2057            ScenePlot::Contour { vertices, .. } | ScenePlot::ContourFill { vertices, .. } => {
2058                if vertices.is_empty() {
2059                    return Err(SceneExportError::unexportable(
2060                        "contour plot has no exportable vertex scene data",
2061                    ));
2062                }
2063            }
2064            ScenePlot::Pie { values, .. } => {
2065                if values.is_empty() {
2066                    return Err(SceneExportError::unexportable(
2067                        "pie plot has no exportable value scene data",
2068                    ));
2069                }
2070            }
2071            ScenePlot::ReferenceLine { .. } => {}
2072            ScenePlot::Unsupported { plot_kind, .. } => {
2073                return Err(SceneExportError::unexportable(format!(
2074                    "unsupported plot kind cannot be exported: {plot_kind:?}"
2075                )));
2076            }
2077        }
2078        Ok(())
2079    }
2080
2081    fn from_plot(plot: &PlotElement, axes_index: u32) -> Self {
2082        match plot {
2083            PlotElement::Line(line) => Self::Line {
2084                x: line.x_data.clone(),
2085                y: line.y_data.clone(),
2086                color_rgba: vec4_to_rgba(line.color),
2087                line_width: line.line_width,
2088                line_style: format!("{:?}", line.line_style),
2089                axes_index,
2090                label: line.label.clone(),
2091                visible: line.visible,
2092            },
2093            PlotElement::ReferenceLine(line) => Self::ReferenceLine {
2094                orientation: match line.orientation {
2095                    ReferenceLineOrientation::Vertical => "vertical",
2096                    ReferenceLineOrientation::Horizontal => "horizontal",
2097                }
2098                .into(),
2099                value: line.value,
2100                color_rgba: vec4_to_rgba(line.color),
2101                line_width: line.line_width,
2102                line_style: format!("{:?}", line.line_style),
2103                label: line.label.clone(),
2104                display_name: line.display_name.clone(),
2105                label_orientation: line.label_orientation.clone(),
2106                axes_index,
2107                visible: line.visible,
2108            },
2109            PlotElement::Scatter(scatter) => Self::Scatter {
2110                x: scatter.x_data.clone(),
2111                y: scatter.y_data.clone(),
2112                color_rgba: vec4_to_rgba(scatter.color),
2113                marker_size: scatter.marker_size,
2114                marker_style: format!("{:?}", scatter.marker_style),
2115                axes_index,
2116                label: scatter.label.clone(),
2117                visible: scatter.visible,
2118            },
2119            PlotElement::Bar(bar) => Self::Bar {
2120                labels: bar.labels.clone(),
2121                values: bar.values().unwrap_or(&[]).to_vec(),
2122                histogram_bin_edges: bar.histogram_bin_edges().map(|edges| edges.to_vec()),
2123                color_rgba: vec4_to_rgba(bar.color),
2124                outline_color_rgba: bar.outline_color.map(vec4_to_rgba),
2125                bar_width: bar.bar_width,
2126                outline_width: bar.outline_width,
2127                orientation: format!("{:?}", bar.orientation),
2128                group_index: bar.group_index as u32,
2129                group_count: bar.group_count as u32,
2130                stack_offsets: bar.stack_offsets().map(|offsets| offsets.to_vec()),
2131                axes_index,
2132                label: bar.label.clone(),
2133                visible: bar.visible,
2134            },
2135            PlotElement::ErrorBar(error) => Self::ErrorBar {
2136                x: error.x.clone(),
2137                y: error.y.clone(),
2138                err_low: error.y_neg.clone(),
2139                err_high: error.y_pos.clone(),
2140                x_err_low: error.x_neg.clone(),
2141                x_err_high: error.x_pos.clone(),
2142                orientation: format!("{:?}", error.orientation),
2143                color_rgba: vec4_to_rgba(error.color),
2144                line_width: error.line_width,
2145                line_style: format!("{:?}", error.line_style),
2146                cap_width: error.cap_size,
2147                marker_style: error.marker.as_ref().map(|m| format!("{:?}", m.kind)),
2148                marker_size: error.marker.as_ref().map(|m| m.size),
2149                marker_face_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.face_color)),
2150                marker_edge_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.edge_color)),
2151                marker_filled: error.marker.as_ref().map(|m| m.filled),
2152                axes_index,
2153                label: error.label.clone(),
2154                visible: error.visible,
2155            },
2156            PlotElement::Stairs(stairs) => Self::Stairs {
2157                x: stairs.x.clone(),
2158                y: stairs.y.clone(),
2159                color_rgba: vec4_to_rgba(stairs.color),
2160                line_width: stairs.line_width,
2161                axes_index,
2162                label: stairs.label.clone(),
2163                visible: stairs.visible,
2164            },
2165            PlotElement::Stem(stem) => Self::Stem {
2166                x: stem.x.clone(),
2167                y: stem.y.clone(),
2168                baseline: stem.baseline,
2169                color_rgba: vec4_to_rgba(stem.color),
2170                line_width: stem.line_width,
2171                line_style: format!("{:?}", stem.line_style),
2172                baseline_color_rgba: vec4_to_rgba(stem.baseline_color),
2173                baseline_visible: stem.baseline_visible,
2174                marker_color_rgba: vec4_to_rgba(
2175                    stem.marker
2176                        .as_ref()
2177                        .map(|m| m.face_color)
2178                        .unwrap_or(stem.color),
2179                ),
2180                marker_size: stem.marker.as_ref().map(|m| m.size).unwrap_or(0.0),
2181                marker_filled: stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
2182                axes_index,
2183                label: stem.label.clone(),
2184                visible: stem.visible,
2185            },
2186            PlotElement::Area(area) => Self::Area {
2187                x: area.x.clone(),
2188                y: area.y.clone(),
2189                lower_y: area.lower_y.clone(),
2190                baseline: area.baseline,
2191                color_rgba: vec4_to_rgba(area.color),
2192                axes_index,
2193                label: area.label.clone(),
2194                visible: area.visible,
2195            },
2196            PlotElement::Quiver(quiver) => Self::Quiver {
2197                x: quiver.x.clone(),
2198                y: quiver.y.clone(),
2199                u: quiver.u.clone(),
2200                v: quiver.v.clone(),
2201                color_rgba: vec4_to_rgba(quiver.color),
2202                line_width: quiver.line_width,
2203                scale: quiver.scale,
2204                head_size: quiver.head_size,
2205                axes_index,
2206                label: quiver.label.clone(),
2207                visible: quiver.visible,
2208            },
2209            PlotElement::Surface(surface) => Self::Surface {
2210                x: surface.x_data.clone(),
2211                y: surface.y_data.clone(),
2212                z: surface.z_data.clone().unwrap_or_default(),
2213                colormap: format!("{:?}", surface.colormap),
2214                shading_mode: format!("{:?}", surface.shading_mode),
2215                wireframe: surface.wireframe,
2216                alpha: surface.alpha,
2217                flatten_z: surface.flatten_z,
2218                image_mode: surface.image_mode,
2219                color_grid_rgba: surface.color_grid.as_ref().map(|grid| {
2220                    grid.iter()
2221                        .map(|row| row.iter().map(|color| vec4_to_rgba(*color)).collect())
2222                        .collect()
2223                }),
2224                color_limits: surface.color_limits.map(|(lo, hi)| [lo, hi]),
2225                axes_index,
2226                label: surface.label.clone(),
2227                visible: surface.visible,
2228            },
2229            PlotElement::Patch(patch) => Self::Patch {
2230                vertices: patch
2231                    .vertices()
2232                    .iter()
2233                    .map(|point| vec3_to_xyz(*point))
2234                    .collect(),
2235                faces: patch
2236                    .faces()
2237                    .iter()
2238                    .map(|face| face.iter().map(|idx| *idx as u32).collect())
2239                    .collect(),
2240                face_color_rgba: vec4_to_rgba(patch.face_color()),
2241                edge_color_rgba: vec4_to_rgba(patch.edge_color()),
2242                face_color_mode: format!("{:?}", patch.face_color_mode()),
2243                edge_color_mode: format!("{:?}", patch.edge_color_mode()),
2244                face_alpha: patch.face_alpha(),
2245                edge_alpha: patch.edge_alpha(),
2246                line_width: patch.line_width(),
2247                axes_index,
2248                label: patch.label().map(str::to_string),
2249                visible: patch.is_visible(),
2250                force_3d: patch.force_3d(),
2251            },
2252            PlotElement::Mesh(mesh) => Self::Mesh {
2253                vertices: mesh
2254                    .vertices()
2255                    .iter()
2256                    .map(|point| vec3_to_xyz(*point))
2257                    .collect(),
2258                triangles: mesh.triangles().to_vec(),
2259                mesh_id: mesh.mesh_id().map(str::to_string),
2260                face_color_rgba: vec4_to_rgba(mesh.face_color()),
2261                edge_color_rgba: vec4_to_rgba(mesh.edge_color()),
2262                face_alpha: mesh.face_alpha(),
2263                edge_alpha: mesh.edge_alpha(),
2264                edge_width: mesh.edge_width(),
2265                edge_mode: mesh.edge_mode().as_str().to_string(),
2266                feature_edge_groups: mesh
2267                    .feature_edge_groups()
2268                    .map(|groups| groups.to_vec())
2269                    .unwrap_or_default(),
2270                vertex_colors_rgba: mesh
2271                    .vertex_colors()
2272                    .map(|colors| colors.iter().copied().map(vec4_to_rgba).collect())
2273                    .unwrap_or_default(),
2274                triangle_colors_rgba: mesh
2275                    .triangle_colors()
2276                    .map(|colors| colors.iter().copied().map(vec4_to_rgba).collect())
2277                    .unwrap_or_default(),
2278                axes_index,
2279                label: mesh.label().map(str::to_string),
2280                regions: mesh.regions().iter().map(Into::into).collect(),
2281                highlighted_region_id: mesh.highlighted_region_id().map(str::to_string),
2282                highlight_color_rgba: Some(vec4_to_rgba(mesh.highlight_color())),
2283                scalar_field: mesh.scalar_field().map(|field| Box::new(field.into())),
2284                vector_field: mesh.vector_field().map(|field| Box::new(field.into())),
2285                deformation: mesh.deformation().map(|field| Box::new(field.into())),
2286                visible: mesh.is_visible(),
2287            },
2288            PlotElement::Line3(line) => Self::Line3 {
2289                x: line.x_data.clone(),
2290                y: line.y_data.clone(),
2291                z: line.z_data.clone(),
2292                color_rgba: vec4_to_rgba(line.color),
2293                line_width: line.line_width,
2294                line_style: format!("{:?}", line.line_style),
2295                axes_index,
2296                label: line.label.clone(),
2297                visible: line.visible,
2298            },
2299            PlotElement::Scatter3(scatter3) => Self::Scatter3 {
2300                points: scatter3
2301                    .points
2302                    .iter()
2303                    .map(|point| vec3_to_xyz(*point))
2304                    .collect(),
2305                colors_rgba: scatter3
2306                    .colors
2307                    .iter()
2308                    .map(|color| vec4_to_rgba(*color))
2309                    .collect(),
2310                point_size: scatter3.point_size,
2311                point_sizes: scatter3.point_sizes.clone(),
2312                axes_index,
2313                label: scatter3.label.clone(),
2314                visible: scatter3.visible,
2315            },
2316            PlotElement::Contour(contour) => Self::Contour {
2317                vertices: contour
2318                    .cpu_vertices()
2319                    .unwrap_or(&[])
2320                    .iter()
2321                    .cloned()
2322                    .map(Into::into)
2323                    .collect(),
2324                bounds_min: vec3_to_xyz(contour.bounds().min),
2325                bounds_max: vec3_to_xyz(contour.bounds().max),
2326                base_z: contour.base_z,
2327                line_width: contour.line_width,
2328                axes_index,
2329                label: contour.label.clone(),
2330                visible: contour.visible,
2331                force_3d: contour.force_3d,
2332            },
2333            PlotElement::ContourFill(fill) => Self::ContourFill {
2334                vertices: fill
2335                    .cpu_vertices()
2336                    .unwrap_or(&[])
2337                    .iter()
2338                    .cloned()
2339                    .map(Into::into)
2340                    .collect(),
2341                bounds_min: vec3_to_xyz(fill.bounds().min),
2342                bounds_max: vec3_to_xyz(fill.bounds().max),
2343                axes_index,
2344                label: fill.label.clone(),
2345                visible: fill.visible,
2346            },
2347            PlotElement::Pie(pie) => Self::Pie {
2348                values: pie.values.clone(),
2349                colors_rgba: pie.colors.iter().map(|c| vec4_to_rgba(*c)).collect(),
2350                slice_labels: pie.slice_labels.clone(),
2351                label_format: pie.label_format.clone(),
2352                explode: pie.explode.clone(),
2353                axes_index,
2354                label: pie.label.clone(),
2355                visible: pie.visible,
2356            },
2357        }
2358    }
2359
2360    fn apply_to_figure(self, figure: &mut Figure) -> Result<(), String> {
2361        match self {
2362            ScenePlot::Line {
2363                x,
2364                y,
2365                color_rgba,
2366                line_width,
2367                line_style,
2368                axes_index,
2369                label,
2370                visible,
2371            } => {
2372                let mut line = LinePlot::new(x, y)?;
2373                line.set_color(rgba_to_vec4(color_rgba));
2374                line.set_line_width(line_width);
2375                line.set_line_style(parse_line_style(&line_style));
2376                line.label = label;
2377                line.set_visible(visible);
2378                figure.add_line_plot_on_axes(line, axes_index as usize);
2379            }
2380            ScenePlot::ReferenceLine {
2381                orientation,
2382                value,
2383                color_rgba,
2384                line_width,
2385                line_style,
2386                label,
2387                display_name,
2388                label_orientation,
2389                axes_index,
2390                visible,
2391            } => {
2392                let orientation = parse_reference_line_orientation(&orientation)?;
2393                let mut line = ReferenceLine::new(orientation, value)?.with_style(
2394                    rgba_to_vec4(color_rgba),
2395                    line_width,
2396                    parse_line_style(&line_style),
2397                );
2398                line.label = label;
2399                line.display_name = display_name;
2400                line.label_orientation = label_orientation;
2401                line.visible = visible;
2402                figure.add_reference_line_on_axes(line, axes_index as usize);
2403            }
2404            ScenePlot::Scatter {
2405                x,
2406                y,
2407                color_rgba,
2408                marker_size,
2409                marker_style,
2410                axes_index,
2411                label,
2412                visible,
2413            } => {
2414                let mut scatter = ScatterPlot::new(x, y)?;
2415                scatter.set_color(rgba_to_vec4(color_rgba));
2416                scatter.set_marker_size(marker_size);
2417                scatter.set_marker_style(parse_marker_style(&marker_style));
2418                scatter.label = label;
2419                scatter.set_visible(visible);
2420                figure.add_scatter_plot_on_axes(scatter, axes_index as usize);
2421            }
2422            ScenePlot::Bar {
2423                labels,
2424                values,
2425                histogram_bin_edges,
2426                color_rgba,
2427                outline_color_rgba,
2428                bar_width,
2429                outline_width,
2430                orientation,
2431                group_index,
2432                group_count,
2433                stack_offsets,
2434                axes_index,
2435                label,
2436                visible,
2437            } => {
2438                let mut bar = BarChart::new(labels, values)?
2439                    .with_style(rgba_to_vec4(color_rgba), bar_width)
2440                    .with_orientation(parse_bar_orientation(&orientation))
2441                    .with_group(group_index as usize, group_count as usize);
2442                if let Some(edges) = histogram_bin_edges {
2443                    bar.set_histogram_bin_edges(edges);
2444                }
2445                if let Some(offsets) = stack_offsets {
2446                    bar = bar.with_stack_offsets(offsets);
2447                }
2448                if let Some(outline) = outline_color_rgba {
2449                    bar = bar.with_outline(rgba_to_vec4(outline), outline_width);
2450                }
2451                bar.label = label;
2452                bar.set_visible(visible);
2453                figure.add_bar_chart_on_axes(bar, axes_index as usize);
2454            }
2455            ScenePlot::ErrorBar {
2456                x,
2457                y,
2458                err_low,
2459                err_high,
2460                x_err_low,
2461                x_err_high,
2462                orientation,
2463                color_rgba,
2464                line_width,
2465                line_style,
2466                cap_width,
2467                marker_style,
2468                marker_size,
2469                marker_face_color,
2470                marker_edge_color,
2471                marker_filled,
2472                axes_index,
2473                label,
2474                visible,
2475            } => {
2476                let mut error = if orientation.eq_ignore_ascii_case("Both") {
2477                    ErrorBar::new_both(x, y, x_err_low, x_err_high, err_low, err_high)?
2478                } else {
2479                    ErrorBar::new_vertical(x, y, err_low, err_high)?
2480                }
2481                .with_style(
2482                    rgba_to_vec4(color_rgba),
2483                    line_width,
2484                    parse_line_style_name(&line_style),
2485                    cap_width,
2486                );
2487                if let Some(size) = marker_size {
2488                    error.set_marker(Some(crate::plots::line::LineMarkerAppearance {
2489                        kind: parse_marker_style(marker_style.as_deref().unwrap_or("Circle")),
2490                        size,
2491                        edge_color: marker_edge_color
2492                            .map(rgba_to_vec4)
2493                            .unwrap_or(rgba_to_vec4(color_rgba)),
2494                        face_color: marker_face_color
2495                            .map(rgba_to_vec4)
2496                            .unwrap_or(rgba_to_vec4(color_rgba)),
2497                        filled: marker_filled.unwrap_or(false),
2498                    }));
2499                }
2500                error.label = label;
2501                error.set_visible(visible);
2502                figure.add_errorbar_on_axes(error, axes_index as usize);
2503            }
2504            ScenePlot::Stairs {
2505                x,
2506                y,
2507                color_rgba,
2508                line_width,
2509                axes_index,
2510                label,
2511                visible,
2512            } => {
2513                let mut stairs = StairsPlot::new(x, y)?;
2514                stairs.color = rgba_to_vec4(color_rgba);
2515                stairs.line_width = line_width;
2516                stairs.label = label;
2517                stairs.set_visible(visible);
2518                figure.add_stairs_plot_on_axes(stairs, axes_index as usize);
2519            }
2520            ScenePlot::Stem {
2521                x,
2522                y,
2523                baseline,
2524                color_rgba,
2525                line_width,
2526                line_style,
2527                baseline_color_rgba,
2528                baseline_visible,
2529                marker_color_rgba,
2530                marker_size,
2531                marker_filled,
2532                axes_index,
2533                label,
2534                visible,
2535            } => {
2536                let mut stem = StemPlot::new(x, y)?;
2537                stem = stem
2538                    .with_style(
2539                        rgba_to_vec4(color_rgba),
2540                        line_width,
2541                        parse_line_style_name(&line_style),
2542                        baseline,
2543                    )
2544                    .with_baseline_style(rgba_to_vec4(baseline_color_rgba), baseline_visible);
2545                if marker_size > 0.0 {
2546                    stem.set_marker(Some(crate::plots::line::LineMarkerAppearance {
2547                        kind: crate::plots::scatter::MarkerStyle::Circle,
2548                        size: marker_size,
2549                        edge_color: rgba_to_vec4(marker_color_rgba),
2550                        face_color: rgba_to_vec4(marker_color_rgba),
2551                        filled: marker_filled,
2552                    }));
2553                }
2554                stem.label = label;
2555                stem.set_visible(visible);
2556                figure.add_stem_plot_on_axes(stem, axes_index as usize);
2557            }
2558            ScenePlot::Area {
2559                x,
2560                y,
2561                lower_y,
2562                baseline,
2563                color_rgba,
2564                axes_index,
2565                label,
2566                visible,
2567            } => {
2568                let mut area = AreaPlot::new(x, y)?;
2569                if let Some(lower_y) = lower_y {
2570                    area = area.with_lower_curve(lower_y);
2571                }
2572                area.baseline = baseline;
2573                area.color = rgba_to_vec4(color_rgba);
2574                area.label = label;
2575                area.set_visible(visible);
2576                figure.add_area_plot_on_axes(area, axes_index as usize);
2577            }
2578            ScenePlot::Quiver {
2579                x,
2580                y,
2581                u,
2582                v,
2583                color_rgba,
2584                line_width,
2585                scale,
2586                head_size,
2587                axes_index,
2588                label,
2589                visible,
2590            } => {
2591                let mut quiver = QuiverPlot::new(x, y, u, v)?
2592                    .with_style(rgba_to_vec4(color_rgba), line_width, scale, head_size)
2593                    .with_label(label.unwrap_or_else(|| "Data".to_string()));
2594                quiver.set_visible(visible);
2595                figure.add_quiver_plot_on_axes(quiver, axes_index as usize);
2596            }
2597            ScenePlot::Surface {
2598                x,
2599                y,
2600                z,
2601                colormap,
2602                shading_mode,
2603                wireframe,
2604                alpha,
2605                flatten_z,
2606                image_mode,
2607                color_grid_rgba,
2608                color_limits,
2609                axes_index,
2610                label,
2611                visible,
2612            } => {
2613                let mut surface = SurfacePlot::new(x, y, z)?;
2614                surface.colormap = parse_colormap(&colormap);
2615                surface.shading_mode = parse_shading_mode(&shading_mode);
2616                surface.wireframe = wireframe;
2617                surface.alpha = alpha.clamp(0.0, 1.0);
2618                surface.flatten_z = flatten_z;
2619                surface.image_mode = image_mode;
2620                surface.color_grid = color_grid_rgba.map(|grid| {
2621                    grid.into_iter()
2622                        .map(|row| row.into_iter().map(rgba_to_vec4).collect())
2623                        .collect()
2624                });
2625                surface.color_limits = color_limits.map(|[lo, hi]| (lo, hi));
2626                surface.label = label;
2627                surface.visible = visible;
2628                figure.add_surface_plot_on_axes(surface, axes_index as usize);
2629            }
2630            ScenePlot::Patch {
2631                vertices,
2632                faces,
2633                face_color_rgba,
2634                edge_color_rgba,
2635                face_color_mode,
2636                edge_color_mode,
2637                face_alpha,
2638                edge_alpha,
2639                line_width,
2640                axes_index,
2641                label,
2642                visible,
2643                force_3d,
2644            } => {
2645                let vertices: Vec<Vec3> = vertices.into_iter().map(xyz_to_vec3).collect();
2646                let faces: Vec<Vec<usize>> = faces
2647                    .into_iter()
2648                    .map(|face| face.into_iter().map(|idx| idx as usize).collect())
2649                    .collect();
2650                let mut patch = PatchPlot::new(vertices, faces)?;
2651                patch.set_face_color(rgba_to_vec4(face_color_rgba));
2652                patch.set_edge_color(rgba_to_vec4(edge_color_rgba));
2653                patch.set_face_color_mode(parse_patch_face_color_mode(&face_color_mode));
2654                patch.set_edge_color_mode(parse_patch_edge_color_mode(&edge_color_mode));
2655                patch.set_face_alpha(face_alpha);
2656                patch.set_edge_alpha(edge_alpha);
2657                patch.set_line_width(line_width);
2658                patch.set_label(label);
2659                patch.set_visible(visible);
2660                patch.set_force_3d(force_3d);
2661                figure.add_patch_plot_on_axes(patch, axes_index as usize);
2662            }
2663            ScenePlot::Mesh {
2664                vertices,
2665                triangles,
2666                mesh_id,
2667                face_color_rgba,
2668                edge_color_rgba,
2669                face_alpha,
2670                edge_alpha,
2671                edge_width,
2672                edge_mode,
2673                feature_edge_groups,
2674                vertex_colors_rgba,
2675                triangle_colors_rgba,
2676                axes_index,
2677                label,
2678                regions,
2679                highlighted_region_id,
2680                highlight_color_rgba,
2681                scalar_field,
2682                vector_field,
2683                deformation,
2684                visible,
2685            } => {
2686                let vertices: Vec<Vec3> = vertices.into_iter().map(xyz_to_vec3).collect();
2687                let mut mesh = MeshPlot::new(vertices, triangles)?;
2688                mesh.set_mesh_id(mesh_id);
2689                mesh.set_face_color(rgba_to_vec4(face_color_rgba));
2690                mesh.set_edge_color(rgba_to_vec4(edge_color_rgba));
2691                mesh.set_face_alpha(face_alpha);
2692                mesh.set_edge_alpha(edge_alpha);
2693                mesh.set_edge_width(edge_width);
2694                mesh.set_edge_mode(parse_mesh_edge_mode(&edge_mode));
2695                if !feature_edge_groups.is_empty() {
2696                    mesh.set_feature_edge_groups(Some(feature_edge_groups))?;
2697                }
2698                if !vertex_colors_rgba.is_empty() {
2699                    mesh.set_vertex_colors(Some(
2700                        vertex_colors_rgba.into_iter().map(rgba_to_vec4).collect(),
2701                    ))?;
2702                }
2703                if !triangle_colors_rgba.is_empty() {
2704                    mesh.set_triangle_colors(Some(
2705                        triangle_colors_rgba.into_iter().map(rgba_to_vec4).collect(),
2706                    ))?;
2707                }
2708                mesh.set_label(label);
2709                mesh.set_regions(regions.into_iter().map(Into::into).collect());
2710                mesh.set_highlighted_region_id(highlighted_region_id);
2711                if let Some(color) = highlight_color_rgba {
2712                    mesh.set_highlight_color(rgba_to_vec4(color));
2713                }
2714                if let Some(field) = scalar_field {
2715                    mesh.set_scalar_field(Some((*field).try_into()?))?;
2716                }
2717                if let Some(field) = vector_field {
2718                    mesh.set_vector_field(Some((*field).try_into()?))?;
2719                }
2720                if let Some(field) = deformation {
2721                    mesh.set_deformation(Some((*field).into()))?;
2722                }
2723                mesh.set_visible(visible);
2724                figure.add_mesh_plot_on_axes(mesh, axes_index as usize);
2725            }
2726            ScenePlot::Line3 {
2727                x,
2728                y,
2729                z,
2730                color_rgba,
2731                line_width,
2732                line_style,
2733                axes_index,
2734                label,
2735                visible,
2736            } => {
2737                let mut plot = Line3Plot::new(x, y, z)?
2738                    .with_style(
2739                        rgba_to_vec4(color_rgba),
2740                        line_width,
2741                        parse_line_style_name(&line_style),
2742                    )
2743                    .with_label(label.unwrap_or_else(|| "Data".to_string()));
2744                plot.set_visible(visible);
2745                figure.add_line3_plot_on_axes(plot, axes_index as usize);
2746            }
2747            ScenePlot::Scatter3 {
2748                points,
2749                colors_rgba,
2750                point_size,
2751                point_sizes,
2752                axes_index,
2753                label,
2754                visible,
2755            } => {
2756                let points: Vec<Vec3> = points.into_iter().map(xyz_to_vec3).collect();
2757                let colors: Vec<Vec4> = colors_rgba.into_iter().map(rgba_to_vec4).collect();
2758                let mut scatter3 = Scatter3Plot::new(points)?;
2759                if !colors.is_empty() {
2760                    scatter3 = scatter3.with_colors(colors)?;
2761                }
2762                scatter3.point_size = point_size.max(1.0);
2763                scatter3.point_sizes = point_sizes;
2764                scatter3.label = label;
2765                scatter3.visible = visible;
2766                figure.add_scatter3_plot_on_axes(scatter3, axes_index as usize);
2767            }
2768            ScenePlot::Contour {
2769                vertices,
2770                bounds_min,
2771                bounds_max,
2772                base_z,
2773                line_width,
2774                axes_index,
2775                label,
2776                visible,
2777                force_3d,
2778            } => {
2779                let mut contour = ContourPlot::from_vertices(
2780                    vertices.into_iter().map(Into::into).collect(),
2781                    base_z,
2782                    serialized_bounds(bounds_min, bounds_max),
2783                )
2784                .with_line_width(line_width)
2785                .with_force_3d(force_3d);
2786                contour.label = label;
2787                contour.set_visible(visible);
2788                figure.add_contour_plot_on_axes(contour, axes_index as usize);
2789            }
2790            ScenePlot::ContourFill {
2791                vertices,
2792                bounds_min,
2793                bounds_max,
2794                axes_index,
2795                label,
2796                visible,
2797            } => {
2798                let mut fill = ContourFillPlot::from_vertices(
2799                    vertices.into_iter().map(Into::into).collect(),
2800                    serialized_bounds(bounds_min, bounds_max),
2801                );
2802                fill.label = label;
2803                fill.set_visible(visible);
2804                figure.add_contour_fill_plot_on_axes(fill, axes_index as usize);
2805            }
2806            ScenePlot::Pie {
2807                values,
2808                colors_rgba,
2809                slice_labels,
2810                label_format,
2811                explode,
2812                axes_index,
2813                label,
2814                visible,
2815            } => {
2816                let mut pie = crate::plots::PieChart::new(
2817                    values,
2818                    Some(colors_rgba.into_iter().map(rgba_to_vec4).collect()),
2819                )?
2820                .with_slice_labels(slice_labels)
2821                .with_explode(explode);
2822                if let Some(fmt) = label_format {
2823                    pie = pie.with_label_format(fmt);
2824                }
2825                pie.label = label;
2826                pie.set_visible(visible);
2827                figure.add_pie_chart_on_axes(pie, axes_index as usize);
2828            }
2829            ScenePlot::Unsupported { .. } => {}
2830        }
2831        Ok(())
2832    }
2833}
2834
2835fn parse_line_style(value: &str) -> crate::plots::LineStyle {
2836    match value {
2837        "Dashed" => crate::plots::LineStyle::Dashed,
2838        "Dotted" => crate::plots::LineStyle::Dotted,
2839        "DashDot" => crate::plots::LineStyle::DashDot,
2840        _ => crate::plots::LineStyle::Solid,
2841    }
2842}
2843
2844fn parse_bar_orientation(value: &str) -> crate::plots::bar::Orientation {
2845    match value {
2846        "Horizontal" => crate::plots::bar::Orientation::Horizontal,
2847        _ => crate::plots::bar::Orientation::Vertical,
2848    }
2849}
2850
2851fn parse_reference_line_orientation(value: &str) -> Result<ReferenceLineOrientation, String> {
2852    match value.to_ascii_lowercase().as_str() {
2853        "horizontal" => Ok(ReferenceLineOrientation::Horizontal),
2854        "vertical" => Ok(ReferenceLineOrientation::Vertical),
2855        _ => Err(format!(
2856            "unknown reference line orientation '{value}'; expected 'horizontal' or 'vertical'"
2857        )),
2858    }
2859}
2860
2861fn parse_marker_style(value: &str) -> MarkerStyle {
2862    match value {
2863        "Square" => MarkerStyle::Square,
2864        "Triangle" => MarkerStyle::Triangle,
2865        "Diamond" => MarkerStyle::Diamond,
2866        "Plus" => MarkerStyle::Plus,
2867        "Cross" => MarkerStyle::Cross,
2868        "Star" => MarkerStyle::Star,
2869        "Hexagon" => MarkerStyle::Hexagon,
2870        _ => MarkerStyle::Circle,
2871    }
2872}
2873
2874fn parse_colormap(value: &str) -> ColorMap {
2875    ColorMap::from_name(value).unwrap_or(ColorMap::Parula)
2876}
2877
2878fn parse_shading_mode(value: &str) -> ShadingMode {
2879    match value {
2880        "Flat" => ShadingMode::Flat,
2881        "Smooth" => ShadingMode::Smooth,
2882        "Faceted" => ShadingMode::Faceted,
2883        "None" => ShadingMode::None,
2884        _ => ShadingMode::Smooth,
2885    }
2886}
2887
2888fn parse_patch_face_color_mode(value: &str) -> PatchFaceColorMode {
2889    match value {
2890        "None" => PatchFaceColorMode::None,
2891        "Flat" => PatchFaceColorMode::Flat,
2892        _ => PatchFaceColorMode::Color,
2893    }
2894}
2895
2896fn parse_patch_edge_color_mode(value: &str) -> PatchEdgeColorMode {
2897    match value {
2898        "None" => PatchEdgeColorMode::None,
2899        _ => PatchEdgeColorMode::Color,
2900    }
2901}
2902
2903fn parse_mesh_edge_mode(value: &str) -> MeshEdgeMode {
2904    MeshEdgeMode::parse(value).unwrap_or_default()
2905}
2906
2907fn xyz_to_vec3(value: [f32; 3]) -> Vec3 {
2908    Vec3::new(value[0], value[1], value[2])
2909}
2910
2911fn serialized_bounds(min: [f32; 3], max: [f32; 3]) -> BoundingBox {
2912    BoundingBox::new(xyz_to_vec3(min), xyz_to_vec3(max))
2913}
2914
2915fn vec3_to_xyz(value: Vec3) -> [f32; 3] {
2916    [value.x, value.y, value.z]
2917}
2918
2919fn rgba_to_vec4(value: [f32; 4]) -> Vec4 {
2920    Vec4::new(value[0], value[1], value[2], value[3])
2921}
2922
2923#[derive(Debug, Clone, Serialize, Deserialize)]
2924#[serde(rename_all = "camelCase")]
2925pub struct SerializedVertex {
2926    position: [f32; 3],
2927    color_rgba: [f32; 4],
2928    normal: [f32; 3],
2929    tex_coords: [f32; 2],
2930}
2931
2932impl From<Vertex> for SerializedVertex {
2933    fn from(value: Vertex) -> Self {
2934        Self {
2935            position: value.position,
2936            color_rgba: value.color,
2937            normal: value.normal,
2938            tex_coords: value.tex_coords,
2939        }
2940    }
2941}
2942
2943impl From<SerializedVertex> for Vertex {
2944    fn from(value: SerializedVertex) -> Self {
2945        Self {
2946            position: value.position,
2947            color: value.color_rgba,
2948            normal: value.normal,
2949            tex_coords: value.tex_coords,
2950        }
2951    }
2952}
2953
2954/// Serialized legend entry for frontend rendering.
2955#[derive(Debug, Clone, Serialize, Deserialize)]
2956#[serde(rename_all = "camelCase")]
2957pub struct FigureLegendEntry {
2958    pub label: String,
2959    pub plot_type: PlotKind,
2960    pub color_rgba: [f32; 4],
2961}
2962
2963impl From<LegendEntry> for FigureLegendEntry {
2964    fn from(entry: LegendEntry) -> Self {
2965        Self {
2966            label: entry.label,
2967            plot_type: PlotKind::from(entry.plot_type),
2968            color_rgba: vec4_to_rgba(entry.color),
2969        }
2970    }
2971}
2972
2973/// Serializable plot kind values consumed by UI + transports.
2974#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2975#[serde(rename_all = "snake_case")]
2976pub enum PlotKind {
2977    Line,
2978    Line3,
2979    Scatter,
2980    Bar,
2981    ErrorBar,
2982    Stairs,
2983    Stem,
2984    Area,
2985    Quiver,
2986    Pie,
2987    Image,
2988    Surface,
2989    Mesh,
2990    Patch,
2991    Scatter3,
2992    Contour,
2993    ContourFill,
2994    ReferenceLine,
2995}
2996
2997impl From<PlotType> for PlotKind {
2998    fn from(value: PlotType) -> Self {
2999        match value {
3000            PlotType::Line => Self::Line,
3001            PlotType::Line3 => Self::Line3,
3002            PlotType::Scatter => Self::Scatter,
3003            PlotType::Bar => Self::Bar,
3004            PlotType::ErrorBar => Self::ErrorBar,
3005            PlotType::Stairs => Self::Stairs,
3006            PlotType::Stem => Self::Stem,
3007            PlotType::Area => Self::Area,
3008            PlotType::Quiver => Self::Quiver,
3009            PlotType::Pie => Self::Pie,
3010            PlotType::Surface => Self::Surface,
3011            PlotType::Mesh => Self::Mesh,
3012            PlotType::Patch => Self::Patch,
3013            PlotType::Scatter3 => Self::Scatter3,
3014            PlotType::Contour => Self::Contour,
3015            PlotType::ContourFill => Self::ContourFill,
3016            PlotType::ReferenceLine => Self::ReferenceLine,
3017        }
3018    }
3019}
3020
3021fn parse_line_style_name(name: &str) -> crate::plots::line::LineStyle {
3022    match name.to_ascii_lowercase().as_str() {
3023        "dashed" => crate::plots::line::LineStyle::Dashed,
3024        "dotted" => crate::plots::line::LineStyle::Dotted,
3025        "dashdot" => crate::plots::line::LineStyle::DashDot,
3026        _ => crate::plots::line::LineStyle::Solid,
3027    }
3028}
3029
3030fn parse_colormap_name(name: &str) -> crate::plots::surface::ColorMap {
3031    crate::plots::surface::ColorMap::from_name(name)
3032        .unwrap_or(crate::plots::surface::ColorMap::Parula)
3033}
3034
3035fn vec4_to_rgba(value: Vec4) -> [f32; 4] {
3036    [value.x, value.y, value.z, value.w]
3037}
3038
3039fn deserialize_f64_lossy<'de, D>(deserializer: D) -> Result<f64, D::Error>
3040where
3041    D: serde::Deserializer<'de>,
3042{
3043    let value = Option::<f64>::deserialize(deserializer)?;
3044    Ok(value.unwrap_or(f64::NAN))
3045}
3046
3047fn deserialize_vec_f64_lossy<'de, D>(deserializer: D) -> Result<Vec<f64>, D::Error>
3048where
3049    D: serde::Deserializer<'de>,
3050{
3051    let values = Vec::<Option<f64>>::deserialize(deserializer)?;
3052    Ok(values
3053        .into_iter()
3054        .map(|value| value.unwrap_or(f64::NAN))
3055        .collect())
3056}
3057
3058fn deserialize_vec_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<f32>, D::Error>
3059where
3060    D: serde::Deserializer<'de>,
3061{
3062    let values = Vec::<Option<f32>>::deserialize(deserializer)?;
3063    Ok(values
3064        .into_iter()
3065        .map(|value| value.unwrap_or(f32::NAN))
3066        .collect())
3067}
3068
3069fn deserialize_option_vec_f64_lossy<'de, D>(deserializer: D) -> Result<Option<Vec<f64>>, D::Error>
3070where
3071    D: serde::Deserializer<'de>,
3072{
3073    let values = Option::<Vec<Option<f64>>>::deserialize(deserializer)?;
3074    Ok(values.map(|items| {
3075        items
3076            .into_iter()
3077            .map(|value| value.unwrap_or(f64::NAN))
3078            .collect()
3079    }))
3080}
3081
3082fn deserialize_matrix_f64_lossy<'de, D>(deserializer: D) -> Result<Vec<Vec<f64>>, D::Error>
3083where
3084    D: serde::Deserializer<'de>,
3085{
3086    let rows = Vec::<Vec<Option<f64>>>::deserialize(deserializer)?;
3087    Ok(rows
3088        .into_iter()
3089        .map(|row| {
3090            row.into_iter()
3091                .map(|value| value.unwrap_or(f64::NAN))
3092                .collect()
3093        })
3094        .collect())
3095}
3096
3097fn deserialize_option_pair_f64_lossy<'de, D>(deserializer: D) -> Result<Option<[f64; 2]>, D::Error>
3098where
3099    D: serde::Deserializer<'de>,
3100{
3101    let value = Option::<[Option<f64>; 2]>::deserialize(deserializer)?;
3102    Ok(value.map(|pair| [pair[0].unwrap_or(f64::NAN), pair[1].unwrap_or(f64::NAN)]))
3103}
3104
3105fn deserialize_option_vec_f32_lossy<'de, D>(deserializer: D) -> Result<Option<Vec<f32>>, D::Error>
3106where
3107    D: serde::Deserializer<'de>,
3108{
3109    let values = Option::<Vec<Option<f32>>>::deserialize(deserializer)?;
3110    Ok(values.map(|items| {
3111        items
3112            .into_iter()
3113            .map(|value| value.unwrap_or(f32::NAN))
3114            .collect()
3115    }))
3116}
3117
3118fn deserialize_vec_xyz_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<[f32; 3]>, D::Error>
3119where
3120    D: serde::Deserializer<'de>,
3121{
3122    let values = Vec::<[Option<f32>; 3]>::deserialize(deserializer)?;
3123    Ok(values
3124        .into_iter()
3125        .map(|xyz| {
3126            [
3127                xyz[0].unwrap_or(f32::NAN),
3128                xyz[1].unwrap_or(f32::NAN),
3129                xyz[2].unwrap_or(f32::NAN),
3130            ]
3131        })
3132        .collect())
3133}
3134
3135fn deserialize_vec_rgba_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<[f32; 4]>, D::Error>
3136where
3137    D: serde::Deserializer<'de>,
3138{
3139    let values = Vec::<[Option<f32>; 4]>::deserialize(deserializer)?;
3140    Ok(values
3141        .into_iter()
3142        .map(|rgba| {
3143            [
3144                rgba[0].unwrap_or(f32::NAN),
3145                rgba[1].unwrap_or(f32::NAN),
3146                rgba[2].unwrap_or(f32::NAN),
3147                rgba[3].unwrap_or(f32::NAN),
3148            ]
3149        })
3150        .collect())
3151}
3152
3153#[cfg(test)]
3154mod tests {
3155    use super::*;
3156    use crate::plots::{
3157        AreaPlot, BarChart, ContourFillPlot, ContourPlot, ErrorBar, Figure, Line3Plot, LinePlot,
3158        MeshPlot, PatchPlot, PieChart, QuiverPlot, ReferenceLine, ReferenceLineOrientation,
3159        Scatter3Plot, ScatterPlot, StairsPlot, StemPlot, SurfacePlot,
3160    };
3161    use glam::{Vec3, Vec4};
3162
3163    #[test]
3164    fn async_scene_export_covers_every_plot_element_variant() {
3165        let bounds = BoundingBox::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
3166        let cases: Vec<(&str, PlotElement)> = vec![
3167            (
3168                "line",
3169                PlotElement::Line(LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap()),
3170            ),
3171            (
3172                "scatter",
3173                PlotElement::Scatter(ScatterPlot::new(vec![0.0, 1.0], vec![2.0, 3.0]).unwrap()),
3174            ),
3175            (
3176                "bar",
3177                PlotElement::Bar(
3178                    BarChart::new(vec!["A".into(), "B".into()], vec![1.0, 2.0]).unwrap(),
3179                ),
3180            ),
3181            (
3182                "errorbar",
3183                PlotElement::ErrorBar(Box::new(
3184                    ErrorBar::new_vertical(
3185                        vec![0.0, 1.0],
3186                        vec![1.0, 2.0],
3187                        vec![0.1, 0.2],
3188                        vec![0.3, 0.4],
3189                    )
3190                    .unwrap(),
3191                )),
3192            ),
3193            (
3194                "stairs",
3195                PlotElement::Stairs(StairsPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap()),
3196            ),
3197            (
3198                "stem",
3199                PlotElement::Stem(StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap()),
3200            ),
3201            (
3202                "area",
3203                PlotElement::Area(AreaPlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap()),
3204            ),
3205            (
3206                "quiver",
3207                PlotElement::Quiver(
3208                    QuiverPlot::new(vec![0.0], vec![0.0], vec![1.0], vec![1.0]).unwrap(),
3209                ),
3210            ),
3211            (
3212                "pie",
3213                PlotElement::Pie(PieChart::new(vec![1.0, 2.0], None).unwrap()),
3214            ),
3215            (
3216                "surface",
3217                PlotElement::Surface(
3218                    SurfacePlot::new(
3219                        vec![0.0, 1.0],
3220                        vec![0.0, 1.0],
3221                        vec![vec![0.0, 1.0], vec![1.0, 2.0]],
3222                    )
3223                    .unwrap(),
3224                ),
3225            ),
3226            (
3227                "mesh",
3228                PlotElement::Mesh(Box::new(
3229                    MeshPlot::new(
3230                        vec![
3231                            Vec3::new(0.0, 0.0, 0.0),
3232                            Vec3::new(1.0, 0.0, 0.0),
3233                            Vec3::new(0.0, 1.0, 0.0),
3234                        ],
3235                        vec![[0, 1, 2]],
3236                    )
3237                    .unwrap(),
3238                )),
3239            ),
3240            (
3241                "patch",
3242                PlotElement::Patch(
3243                    PatchPlot::new(
3244                        vec![
3245                            Vec3::new(0.0, 0.0, 0.0),
3246                            Vec3::new(1.0, 0.0, 0.0),
3247                            Vec3::new(0.0, 1.0, 0.0),
3248                        ],
3249                        vec![vec![0, 1, 2]],
3250                    )
3251                    .unwrap(),
3252                ),
3253            ),
3254            (
3255                "line3",
3256                PlotElement::Line3(
3257                    Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0]).unwrap(),
3258                ),
3259            ),
3260            (
3261                "scatter3",
3262                PlotElement::Scatter3(Scatter3Plot::new(vec![Vec3::new(0.0, 0.0, 0.0)]).unwrap()),
3263            ),
3264            (
3265                "contour",
3266                PlotElement::Contour(ContourPlot::from_vertices(
3267                    vec![
3268                        Vertex::new(Vec3::new(0.0, 0.0, 0.0), Vec4::ONE),
3269                        Vertex::new(Vec3::new(1.0, 1.0, 0.0), Vec4::ONE),
3270                    ],
3271                    0.0,
3272                    bounds,
3273                )),
3274            ),
3275            (
3276                "contour_fill",
3277                PlotElement::ContourFill(ContourFillPlot::from_vertices(
3278                    vec![
3279                        Vertex::new(Vec3::new(0.0, 0.0, 0.0), Vec4::ONE),
3280                        Vertex::new(Vec3::new(1.0, 0.0, 0.0), Vec4::ONE),
3281                        Vertex::new(Vec3::new(0.0, 1.0, 0.0), Vec4::ONE),
3282                    ],
3283                    bounds,
3284                )),
3285            ),
3286            (
3287                "reference_line",
3288                PlotElement::ReferenceLine(
3289                    ReferenceLine::new(ReferenceLineOrientation::Vertical, 0.5).unwrap(),
3290                ),
3291            ),
3292        ];
3293
3294        for (name, plot) in cases {
3295            let scene_plot = futures::executor::block_on(ScenePlot::from_plot_for_export(&plot, 0))
3296                .unwrap_or_else(|err| panic!("{name} export failed: {err}"));
3297            scene_plot
3298                .validate_exportable()
3299                .unwrap_or_else(|err| panic!("{name} validation failed: {err}"));
3300        }
3301    }
3302
3303    #[test]
3304    fn capture_snapshot_reflects_layout_and_metadata() {
3305        let mut figure = Figure::new()
3306            .with_title("Demo")
3307            .with_sg_title("Overview")
3308            .with_labels("X", "Y")
3309            .with_grid(false)
3310            .with_subplot_grid(1, 2);
3311        figure.set_name("Window Name");
3312        figure.set_number_title(false);
3313        figure.set_visible(false);
3314        figure.set_background_color(Vec4::new(0.0, 0.0, 0.0, 1.0));
3315        let line = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
3316        figure.add_line_plot_on_axes(line, 1);
3317
3318        let snapshot = FigureSnapshot::capture(&figure);
3319        assert_eq!(snapshot.layout.axes_rows, 1);
3320        assert_eq!(snapshot.layout.axes_cols, 2);
3321        assert_eq!(snapshot.metadata.title.as_deref(), Some("Demo"));
3322        assert_eq!(snapshot.metadata.name.as_deref(), Some("Window Name"));
3323        assert!(!snapshot.metadata.number_title);
3324        assert!(!snapshot.metadata.visible);
3325        assert_eq!(snapshot.metadata.sg_title.as_deref(), Some("Overview"));
3326        assert_eq!(snapshot.metadata.background_rgba, [0.0, 0.0, 0.0, 1.0]);
3327        assert_eq!(snapshot.metadata.legend_entries.len(), 0);
3328        assert_eq!(snapshot.plots.len(), 1);
3329        assert_eq!(snapshot.plots[0].axes_index, 1);
3330        assert!(!snapshot.metadata.grid_enabled);
3331    }
3332
3333    #[test]
3334    fn surface_scene_validation_uses_surface_plot_orientation() {
3335        let scene_plot = ScenePlot::Surface {
3336            x: vec![0.0, 1.0, 2.0],
3337            y: vec![10.0, 20.0],
3338            z: vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]],
3339            colormap: "Parula".to_string(),
3340            shading_mode: "Smooth".to_string(),
3341            wireframe: false,
3342            alpha: 1.0,
3343            flatten_z: false,
3344            image_mode: false,
3345            color_grid_rgba: Some(vec![
3346                vec![[1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 0.0, 1.0]],
3347                vec![[0.0, 0.0, 1.0, 1.0], [1.0, 1.0, 0.0, 1.0]],
3348                vec![[1.0, 0.0, 1.0, 1.0], [0.0, 1.0, 1.0, 1.0]],
3349            ]),
3350            color_limits: None,
3351            axes_index: 0,
3352            label: None,
3353            visible: true,
3354        };
3355        scene_plot.validate_exportable().unwrap();
3356
3357        let transposed = ScenePlot::Surface {
3358            x: vec![0.0, 1.0, 2.0],
3359            y: vec![10.0, 20.0],
3360            z: vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]],
3361            colormap: "Parula".to_string(),
3362            shading_mode: "Smooth".to_string(),
3363            wireframe: false,
3364            alpha: 1.0,
3365            flatten_z: false,
3366            image_mode: false,
3367            color_grid_rgba: None,
3368            color_limits: None,
3369            axes_index: 0,
3370            label: None,
3371            visible: true,
3372        };
3373        let err = transposed.validate_exportable().unwrap_err();
3374        assert!(err
3375            .to_string()
3376            .contains("row count (2) must match x length (3)"));
3377    }
3378
3379    #[test]
3380    fn image_mode_surface_scene_roundtrip_preserves_color_grid() {
3381        let snapshot = FigureSnapshot::capture(&Figure::new());
3382        let scene = FigureScene {
3383            schema_version: FigureScene::SCHEMA_VERSION,
3384            layout: snapshot.layout,
3385            metadata: snapshot.metadata,
3386            plots: vec![ScenePlot::Surface {
3387                x: vec![0.0, 1.0],
3388                y: vec![10.0, 20.0, 30.0],
3389                z: vec![vec![0.0, 0.0, 0.0], vec![0.0, 0.0, 0.0]],
3390                colormap: "Parula".to_string(),
3391                shading_mode: "None".to_string(),
3392                wireframe: false,
3393                alpha: 1.0,
3394                flatten_z: true,
3395                image_mode: true,
3396                color_grid_rgba: Some(vec![
3397                    vec![
3398                        [1.0, 0.0, 0.0, 1.0],
3399                        [0.0, 1.0, 0.0, 1.0],
3400                        [0.0, 0.0, 1.0, 1.0],
3401                    ],
3402                    vec![
3403                        [1.0, 1.0, 0.0, 1.0],
3404                        [1.0, 0.0, 1.0, 1.0],
3405                        [0.0, 1.0, 1.0, 1.0],
3406                    ],
3407                ]),
3408                color_limits: None,
3409                axes_index: 0,
3410                label: None,
3411                visible: true,
3412            }],
3413        };
3414
3415        let rebuilt = scene.into_figure().expect("image surface scene restores");
3416        let Some(PlotElement::Surface(surface)) = rebuilt.plots().next() else {
3417            panic!("expected surface plot");
3418        };
3419        assert!(surface.image_mode);
3420        assert!(surface.flatten_z);
3421        let grid = surface.color_grid.as_ref().expect("color grid");
3422        assert_eq!(grid.len(), 2);
3423        assert_eq!(grid[0].len(), 3);
3424        assert_eq!(grid[1][2], Vec4::new(0.0, 1.0, 1.0, 1.0));
3425    }
3426
3427    #[test]
3428    fn sg_title_style_omitted_when_sg_title_absent() {
3429        let figure = Figure::new().with_title("Only regular title");
3430        let snapshot = FigureSnapshot::capture(&figure);
3431        assert!(snapshot.metadata.sg_title.is_none());
3432        assert!(
3433            snapshot.metadata.sg_title_style.is_none(),
3434            "sgTitleStyle must be None when sgTitle is absent"
3435        );
3436        let json = serde_json::to_string(&snapshot.metadata).unwrap();
3437        assert!(
3438            !json.contains("sgTitleStyle"),
3439            "sgTitleStyle must not appear in serialized JSON when sgTitle is absent"
3440        );
3441    }
3442
3443    #[test]
3444    fn figure_scene_roundtrip_reconstructs_supported_plots() {
3445        let mut figure = Figure::new().with_title("Replay").with_subplot_grid(1, 2);
3446        figure.set_name("Roundtrip");
3447        figure.set_number_title(false);
3448        figure.set_visible(false);
3449        let mut line = LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap();
3450        line.label = Some("line".to_string());
3451        figure.add_line_plot_on_axes(line, 0);
3452        let mut scatter = ScatterPlot::new(vec![0.0, 1.0, 2.0], vec![2.0, 3.0, 4.0]).unwrap();
3453        scatter.label = Some("scatter".to_string());
3454        figure.add_scatter_plot_on_axes(scatter, 1);
3455
3456        let scene = FigureScene::capture(&figure);
3457        let rebuilt = scene.into_figure().expect("scene restore should succeed");
3458        assert_eq!(rebuilt.axes_grid(), (1, 2));
3459        assert_eq!(rebuilt.plots().count(), 2);
3460        assert_eq!(rebuilt.title.as_deref(), Some("Replay"));
3461        assert_eq!(rebuilt.name.as_deref(), Some("Roundtrip"));
3462        assert!(!rebuilt.number_title);
3463        assert!(!rebuilt.visible);
3464    }
3465
3466    #[test]
3467    fn figure_scene_roundtrip_reconstructs_patch() {
3468        let mut figure = Figure::new();
3469        let mut patch = PatchPlot::new(
3470            vec![
3471                Vec3::new(0.0, 0.0, 0.0),
3472                Vec3::new(1.0, 0.0, 0.0),
3473                Vec3::new(0.0, 1.0, 0.0),
3474            ],
3475            vec![vec![0, 1, 2]],
3476        )
3477        .unwrap();
3478        patch.set_label(Some("tri".into()));
3479        patch.set_force_3d(true);
3480        figure.add_patch_plot(patch);
3481
3482        let scene = FigureScene::capture(&figure);
3483        assert_eq!(scene.schema_version, FigureScene::SCHEMA_VERSION);
3484        assert!(matches!(scene.plots.first(), Some(ScenePlot::Patch { .. })));
3485        let rebuilt = scene.into_figure().expect("patch scene restore");
3486        let Some(PlotElement::Patch(patch)) = rebuilt.plots().next() else {
3487            panic!("expected patch plot");
3488        };
3489        assert_eq!(patch.faces(), &[vec![0, 1, 2]]);
3490        assert_eq!(patch.label(), Some("tri"));
3491        assert!(patch.force_3d());
3492    }
3493
3494    #[test]
3495    fn figure_scene_rejects_invalid_schema_versions() {
3496        let mut scene = FigureScene::capture(&Figure::new());
3497        scene.schema_version = 0;
3498        let err = scene.clone().into_figure().expect_err("schema 0 must fail");
3499        assert!(err.contains("unsupported figure scene schema version 0"));
3500
3501        scene.schema_version = FigureScene::SCHEMA_VERSION + 1;
3502        let err = scene.into_figure().expect_err("future schema must fail");
3503        assert!(err.contains(&format!(
3504            "unsupported figure scene schema version {}",
3505            FigureScene::SCHEMA_VERSION + 1
3506        )));
3507    }
3508
3509    #[test]
3510    fn figure_scene_rejects_patch_in_older_schema() {
3511        let mut figure = Figure::new();
3512        figure.add_patch_plot(
3513            PatchPlot::new(
3514                vec![
3515                    Vec3::new(0.0, 0.0, 0.0),
3516                    Vec3::new(1.0, 0.0, 0.0),
3517                    Vec3::new(0.0, 1.0, 0.0),
3518                ],
3519                vec![vec![0, 1, 2]],
3520            )
3521            .unwrap(),
3522        );
3523
3524        let mut scene = FigureScene::capture(&figure);
3525        assert!(matches!(scene.plots.first(), Some(ScenePlot::Patch { .. })));
3526        scene.schema_version = 1;
3527
3528        let err = scene
3529            .into_figure()
3530            .expect_err("older patch schema must fail");
3531        assert!(err.contains("patch plots require figure scene schema version 2"));
3532    }
3533
3534    #[test]
3535    fn figure_scene_roundtrip_preserves_mesh_plot() {
3536        let mut figure = Figure::new();
3537        let mut mesh = MeshPlot::new(
3538            vec![
3539                Vec3::new(0.0, 0.0, 0.0),
3540                Vec3::new(1.0, 0.0, 0.0),
3541                Vec3::new(0.0, 1.0, 0.0),
3542            ],
3543            vec![[0, 1, 2]],
3544        )
3545        .unwrap();
3546        mesh.set_mesh_id(Some("mesh_1".to_string()));
3547        mesh.set_label(Some("mesh tri".to_string()));
3548        mesh.set_face_alpha(0.7);
3549        mesh.set_edge_width(0.25);
3550        mesh.set_edge_mode(MeshEdgeMode::Feature);
3551        mesh.set_feature_edge_groups(Some(vec![3]))
3552            .expect("feature group should be accepted");
3553        mesh.set_vertex_colors(Some(vec![
3554            Vec4::new(0.3, 0.4, 0.5, 1.0),
3555            Vec4::new(0.3, 0.4, 0.5, 1.0),
3556            Vec4::new(0.3, 0.4, 0.5, 1.0),
3557        ]))
3558        .expect("vertex colors should be accepted");
3559        mesh.set_triangle_colors(Some(vec![Vec4::new(0.3, 0.4, 0.5, 1.0)]))
3560            .expect("triangle color should be accepted");
3561        mesh.set_regions(vec![MeshRegion::new(
3562            "region_default",
3563            Some("Default Region".to_string()),
3564            Some("mesh_default".to_string()),
3565            vec![MeshTriangleRange::new(0, 1)],
3566        )]);
3567        mesh.set_highlighted_region_id(Some("region_default".to_string()));
3568        figure.add_mesh_plot(mesh);
3569
3570        let scene = FigureScene::capture(&figure);
3571        assert_eq!(scene.schema_version, FigureScene::SCHEMA_VERSION);
3572        assert!(matches!(scene.plots.first(), Some(ScenePlot::Mesh { .. })));
3573        let rebuilt = scene.into_figure().expect("mesh scene restore");
3574        let Some(PlotElement::Mesh(mesh)) = rebuilt.plots().next() else {
3575            panic!("expected mesh plot");
3576        };
3577        assert_eq!(mesh.mesh_id(), Some("mesh_1"));
3578        assert_eq!(mesh.triangles(), &[[0, 1, 2]]);
3579        assert_eq!(mesh.label(), Some("mesh tri"));
3580        assert!((mesh.face_alpha() - 0.7).abs() < f32::EPSILON);
3581        assert!((mesh.edge_width() - 0.25).abs() < f32::EPSILON);
3582        assert_eq!(mesh.edge_mode(), MeshEdgeMode::Feature);
3583        assert_eq!(mesh.feature_edge_groups().unwrap(), &[3]);
3584        assert_eq!(
3585            mesh.vertex_colors()
3586                .and_then(|colors| colors.first().copied()),
3587            Some(Vec4::new(0.3, 0.4, 0.5, 1.0))
3588        );
3589        assert_eq!(
3590            mesh.triangle_colors()
3591                .and_then(|colors| colors.first().copied()),
3592            Some(Vec4::new(0.3, 0.4, 0.5, 1.0))
3593        );
3594        assert_eq!(mesh.regions().len(), 1);
3595        assert_eq!(mesh.regions()[0].region_id, "region_default");
3596        assert_eq!(mesh.highlighted_region_id(), Some("region_default"));
3597    }
3598
3599    #[test]
3600    fn figure_scene_roundtrip_preserves_mesh_fea_overlays() {
3601        let mut figure = Figure::new();
3602        let mut mesh = MeshPlot::new(
3603            vec![
3604                Vec3::new(0.0, 0.0, 0.0),
3605                Vec3::new(1.0, 0.0, 0.0),
3606                Vec3::new(0.0, 1.0, 0.0),
3607            ],
3608            vec![[0, 1, 2]],
3609        )
3610        .unwrap();
3611        mesh.set_scalar_field(Some(MeshScalarField {
3612            field_id: "fea.structural.von_mises".to_string(),
3613            label: Some("Von Mises".to_string()),
3614            location: MeshFieldLocation::Vertex,
3615            values: vec![0.0, 0.5, 1.0],
3616            color_limits: Some([0.0, 1.0]),
3617            colormap: "viridis".to_string(),
3618            alpha: 0.8,
3619        }))
3620        .unwrap();
3621        mesh.set_vector_field(Some(MeshVectorField {
3622            field_id: "fea.em.flux_density".to_string(),
3623            label: Some("Flux density".to_string()),
3624            location: MeshFieldLocation::Triangle,
3625            vectors: vec![Vec3::new(0.0, 0.0, 1.0)],
3626            scale: 0.25,
3627            stride: 1,
3628            color: Vec4::new(0.9, 0.7, 0.2, 1.0),
3629        }))
3630        .unwrap();
3631        mesh.set_deformation(Some(MeshDeformation {
3632            field_id: "fea.structural.displacement".to_string(),
3633            label: Some("Displacement".to_string()),
3634            displacements: vec![Vec3::ZERO, Vec3::Z, Vec3::ZERO],
3635            scale: 0.5,
3636        }))
3637        .unwrap();
3638        figure.add_mesh_plot(mesh);
3639
3640        let rebuilt = FigureScene::capture(&figure)
3641            .into_figure()
3642            .expect("mesh scene restore");
3643        let Some(PlotElement::Mesh(mesh)) = rebuilt.plots().next() else {
3644            panic!("expected mesh plot");
3645        };
3646        assert_eq!(
3647            mesh.scalar_field().map(|field| field.field_id.as_str()),
3648            Some("fea.structural.von_mises")
3649        );
3650        assert_eq!(
3651            mesh.vector_field().map(|field| field.field_id.as_str()),
3652            Some("fea.em.flux_density")
3653        );
3654        assert_eq!(
3655            mesh.deformation().map(|field| field.field_id.as_str()),
3656            Some("fea.structural.displacement")
3657        );
3658    }
3659
3660    #[test]
3661    fn figure_scene_rejects_mesh_in_older_schema() {
3662        let mut figure = Figure::new();
3663        figure.add_mesh_plot(
3664            MeshPlot::new(
3665                vec![
3666                    Vec3::new(0.0, 0.0, 0.0),
3667                    Vec3::new(1.0, 0.0, 0.0),
3668                    Vec3::new(0.0, 1.0, 0.0),
3669                ],
3670                vec![[0, 1, 2]],
3671            )
3672            .unwrap(),
3673        );
3674
3675        let mut scene = FigureScene::capture(&figure);
3676        assert!(matches!(scene.plots.first(), Some(ScenePlot::Mesh { .. })));
3677        scene.schema_version = 2;
3678
3679        let err = scene
3680            .into_figure()
3681            .expect_err("older mesh schema must fail");
3682        assert!(err.contains("mesh plots require figure scene schema version 3"));
3683    }
3684
3685    #[test]
3686    fn figure_scene_rejects_unknown_reference_line_orientation() {
3687        let mut scene = FigureScene::capture(&Figure::new());
3688        scene.plots.push(ScenePlot::ReferenceLine {
3689            orientation: "VERTICAL".into(),
3690            value: 2.0,
3691            color_rgba: [0.1, 0.2, 0.3, 1.0],
3692            line_width: 1.0,
3693            line_style: "Solid".into(),
3694            label: None,
3695            display_name: None,
3696            label_orientation: "horizontal".into(),
3697            axes_index: 0,
3698            visible: true,
3699        });
3700
3701        let rebuilt = scene.clone().into_figure().expect("valid orientation");
3702        let PlotElement::ReferenceLine(line) = rebuilt.plots().next().unwrap() else {
3703            panic!("expected reference line")
3704        };
3705        assert!(matches!(
3706            line.orientation,
3707            ReferenceLineOrientation::Vertical
3708        ));
3709
3710        let ScenePlot::ReferenceLine { orientation, .. } = &mut scene.plots[0] else {
3711            panic!("expected reference line scene plot")
3712        };
3713        *orientation = "diagonal".into();
3714
3715        let err = scene
3716            .into_figure()
3717            .expect_err("unknown orientation must fail");
3718        assert!(err.contains("unknown reference line orientation 'diagonal'"));
3719    }
3720
3721    #[test]
3722    fn figure_scene_roundtrip_reconstructs_surface_and_scatter3() {
3723        let mut figure = Figure::new().with_title("Replay3D").with_subplot_grid(1, 2);
3724        let mut surface = SurfacePlot::new(
3725            vec![0.0, 1.0],
3726            vec![0.0, 1.0],
3727            vec![vec![0.0, 1.0], vec![1.0, 2.0]],
3728        )
3729        .expect("surface data should be valid");
3730        surface.label = Some("surface".to_string());
3731        figure.add_surface_plot_on_axes(surface, 0);
3732
3733        let mut scatter3 = Scatter3Plot::new(vec![
3734            Vec3::new(0.0, 0.0, 0.0),
3735            Vec3::new(1.0, 2.0, 3.0),
3736            Vec3::new(2.0, 3.0, 4.0),
3737        ])
3738        .expect("scatter3 data should be valid");
3739        scatter3.label = Some("scatter3".to_string());
3740        figure.add_scatter3_plot_on_axes(scatter3, 1);
3741
3742        let scene = FigureScene::capture(&figure);
3743        let rebuilt = scene.into_figure().expect("scene restore should succeed");
3744        assert_eq!(rebuilt.axes_grid(), (1, 2));
3745        assert_eq!(rebuilt.plots().count(), 2);
3746        assert_eq!(rebuilt.title.as_deref(), Some("Replay3D"));
3747        assert!(matches!(
3748            rebuilt.plots().next(),
3749            Some(PlotElement::Surface(_))
3750        ));
3751        assert!(matches!(
3752            rebuilt.plots().nth(1),
3753            Some(PlotElement::Scatter3(_))
3754        ));
3755    }
3756
3757    #[test]
3758    fn figure_scene_roundtrip_preserves_line3_plot() {
3759        let mut figure = Figure::new();
3760        let line3 = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0])
3761            .unwrap()
3762            .with_label("Trajectory");
3763        figure.add_line3_plot(line3);
3764
3765        let rebuilt = FigureScene::capture(&figure)
3766            .into_figure()
3767            .expect("scene restore should succeed");
3768
3769        let PlotElement::Line3(line3) = rebuilt.plots().next().unwrap() else {
3770            panic!("expected line3")
3771        };
3772        assert_eq!(line3.x_data, vec![0.0, 1.0]);
3773        assert_eq!(line3.z_data, vec![2.0, 3.0]);
3774        assert_eq!(line3.label.as_deref(), Some("Trajectory"));
3775    }
3776
3777    #[test]
3778    fn figure_scene_roundtrip_preserves_contour_and_fill_plots() {
3779        let mut figure = Figure::new();
3780        let bounds = BoundingBox::new(Vec3::new(-1.0, -2.0, 0.0), Vec3::new(3.0, 4.0, 0.0));
3781        let vertices = vec![Vertex {
3782            position: [0.0, 0.0, 0.0],
3783            color: [1.0, 0.0, 0.0, 1.0],
3784            normal: [0.0, 0.0, 1.0],
3785            tex_coords: [0.0, 0.0],
3786        }];
3787        let fill = ContourFillPlot::from_vertices(vertices.clone(), bounds).with_label("fill");
3788        let contour = ContourPlot::from_vertices(vertices, 0.0, bounds)
3789            .with_label("lines")
3790            .with_line_width(2.0);
3791        figure.add_contour_fill_plot(fill);
3792        figure.add_contour_plot(contour);
3793
3794        let rebuilt = FigureScene::capture(&figure)
3795            .into_figure()
3796            .expect("scene restore should succeed");
3797        assert!(matches!(
3798            rebuilt.plots().next(),
3799            Some(PlotElement::ContourFill(_))
3800        ));
3801        let Some(PlotElement::Contour(contour)) = rebuilt.plots().nth(1) else {
3802            panic!("expected contour")
3803        };
3804        assert_eq!(contour.line_width, 2.0);
3805    }
3806
3807    #[test]
3808    fn figure_scene_roundtrip_preserves_stem_style_surface() {
3809        let mut figure = Figure::new();
3810        let mut stem = StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
3811            .unwrap()
3812            .with_style(
3813                Vec4::new(1.0, 0.0, 0.0, 1.0),
3814                2.0,
3815                crate::plots::line::LineStyle::Dashed,
3816                -1.0,
3817            )
3818            .with_baseline_style(Vec4::new(0.0, 0.0, 0.0, 1.0), false)
3819            .with_label("Impulse");
3820        stem.set_marker(Some(crate::plots::line::LineMarkerAppearance {
3821            kind: crate::plots::scatter::MarkerStyle::Square,
3822            size: 8.0,
3823            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
3824            face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
3825            filled: true,
3826        }));
3827        figure.add_stem_plot(stem);
3828
3829        let rebuilt = FigureScene::capture(&figure)
3830            .into_figure()
3831            .expect("scene restore should succeed");
3832        let PlotElement::Stem(stem) = rebuilt.plots().next().unwrap() else {
3833            panic!("expected stem")
3834        };
3835        assert_eq!(stem.baseline, -1.0);
3836        assert_eq!(stem.line_width, 2.0);
3837        assert_eq!(stem.label.as_deref(), Some("Impulse"));
3838        assert!(!stem.baseline_visible);
3839        assert!(stem.marker.as_ref().map(|m| m.filled).unwrap_or(false));
3840        assert_eq!(stem.marker.as_ref().map(|m| m.size), Some(8.0));
3841    }
3842
3843    #[test]
3844    fn figure_scene_roundtrip_preserves_bar_plot() {
3845        let mut figure = Figure::new();
3846        let bar = BarChart::new(vec!["A".into(), "B".into()], vec![2.0, 3.5])
3847            .unwrap()
3848            .with_style(Vec4::new(0.2, 0.4, 0.8, 1.0), 0.95)
3849            .with_outline(Vec4::new(0.1, 0.1, 0.1, 1.0), 1.5)
3850            .with_label("Histogram")
3851            .with_stack_offsets(vec![1.0, 0.5]);
3852        figure.add_bar_chart(bar);
3853
3854        let rebuilt = FigureScene::capture(&figure)
3855            .into_figure()
3856            .expect("scene restore should succeed");
3857        let PlotElement::Bar(bar) = rebuilt.plots().next().unwrap() else {
3858            panic!("expected bar")
3859        };
3860        assert_eq!(bar.labels, vec!["A", "B"]);
3861        assert_eq!(bar.values().unwrap_or(&[]), &[2.0, 3.5]);
3862        assert_eq!(bar.bar_width, 0.95);
3863        assert_eq!(bar.outline_width, 1.5);
3864        assert_eq!(bar.label.as_deref(), Some("Histogram"));
3865        assert_eq!(bar.stack_offsets().unwrap_or(&[]), &[1.0, 0.5]);
3866        assert!(bar.histogram_bin_edges().is_none());
3867    }
3868
3869    #[test]
3870    fn figure_scene_roundtrip_preserves_histogram_bin_edges() {
3871        let mut figure = Figure::new();
3872        let mut bar = BarChart::new(vec!["bin1".into(), "bin2".into()], vec![4.0, 5.0]).unwrap();
3873        bar.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
3874        figure.add_bar_chart(bar);
3875
3876        let rebuilt = FigureScene::capture(&figure)
3877            .into_figure()
3878            .expect("scene restore should succeed");
3879        let PlotElement::Bar(bar) = rebuilt.plots().next().unwrap() else {
3880            panic!("expected bar")
3881        };
3882        assert_eq!(bar.histogram_bin_edges().unwrap_or(&[]), &[0.0, 0.5, 1.0]);
3883    }
3884
3885    #[test]
3886    fn figure_scene_roundtrip_preserves_errorbar_style_surface() {
3887        let mut figure = Figure::new();
3888        let mut error = ErrorBar::new_vertical(
3889            vec![0.0, 1.0],
3890            vec![1.0, 2.0],
3891            vec![0.1, 0.2],
3892            vec![0.2, 0.3],
3893        )
3894        .unwrap()
3895        .with_style(
3896            Vec4::new(1.0, 0.0, 0.0, 1.0),
3897            2.0,
3898            crate::plots::line::LineStyle::Dashed,
3899            10.0,
3900        )
3901        .with_label("Err");
3902        error.set_marker(Some(crate::plots::line::LineMarkerAppearance {
3903            kind: crate::plots::scatter::MarkerStyle::Triangle,
3904            size: 8.0,
3905            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
3906            face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
3907            filled: true,
3908        }));
3909        figure.add_errorbar(error);
3910
3911        let rebuilt = FigureScene::capture(&figure)
3912            .into_figure()
3913            .expect("scene restore should succeed");
3914        let PlotElement::ErrorBar(error) = rebuilt.plots().next().unwrap() else {
3915            panic!("expected errorbar")
3916        };
3917        assert_eq!(error.line_width, 2.0);
3918        assert_eq!(error.cap_size, 10.0);
3919        assert_eq!(error.label.as_deref(), Some("Err"));
3920        assert_eq!(error.line_style, crate::plots::line::LineStyle::Dashed);
3921        assert!(error.marker.as_ref().map(|m| m.filled).unwrap_or(false));
3922    }
3923
3924    #[test]
3925    fn figure_scene_roundtrip_preserves_errorbar_both_direction() {
3926        let mut figure = Figure::new();
3927        let error = ErrorBar::new_both(
3928            vec![1.0, 2.0],
3929            vec![3.0, 4.0],
3930            vec![0.1, 0.2],
3931            vec![0.2, 0.3],
3932            vec![0.3, 0.4],
3933            vec![0.4, 0.5],
3934        )
3935        .unwrap();
3936        figure.add_errorbar(error);
3937        let rebuilt = FigureScene::capture(&figure)
3938            .into_figure()
3939            .expect("scene restore should succeed");
3940        let PlotElement::ErrorBar(error) = rebuilt.plots().next().unwrap() else {
3941            panic!("expected errorbar")
3942        };
3943        assert_eq!(
3944            error.orientation,
3945            crate::plots::errorbar::ErrorBarOrientation::Both
3946        );
3947        assert_eq!(error.x_neg, vec![0.1, 0.2]);
3948        assert_eq!(error.x_pos, vec![0.2, 0.3]);
3949    }
3950
3951    #[test]
3952    fn figure_scene_roundtrip_preserves_quiver_plot() {
3953        let mut figure = Figure::new();
3954        let quiver = QuiverPlot::new(
3955            vec![0.0, 1.0],
3956            vec![1.0, 2.0],
3957            vec![0.5, -0.5],
3958            vec![1.0, 0.25],
3959        )
3960        .unwrap()
3961        .with_style(Vec4::new(0.2, 0.3, 0.4, 1.0), 2.0, 1.5, 0.2)
3962        .with_label("Field");
3963        figure.add_quiver_plot(quiver);
3964
3965        let rebuilt = FigureScene::capture(&figure)
3966            .into_figure()
3967            .expect("scene restore should succeed");
3968        let PlotElement::Quiver(quiver) = rebuilt.plots().next().unwrap() else {
3969            panic!("expected quiver")
3970        };
3971        assert_eq!(quiver.u, vec![0.5, -0.5]);
3972        assert_eq!(quiver.v, vec![1.0, 0.25]);
3973        assert_eq!(quiver.line_width, 2.0);
3974        assert_eq!(quiver.scale, 1.5);
3975        assert_eq!(quiver.head_size, 0.2);
3976        assert_eq!(quiver.label.as_deref(), Some("Field"));
3977    }
3978
3979    #[test]
3980    fn figure_scene_roundtrip_preserves_image_surface_mode_and_color_grid() {
3981        let mut figure = Figure::new();
3982        let surface = SurfacePlot::new(
3983            vec![0.0, 1.0],
3984            vec![0.0, 1.0],
3985            vec![vec![0.0, 0.0], vec![0.0, 0.0]],
3986        )
3987        .unwrap()
3988        .with_flatten_z(true)
3989        .with_image_mode(true)
3990        .with_color_grid(vec![
3991            vec![Vec4::new(1.0, 0.0, 0.0, 1.0), Vec4::new(0.0, 1.0, 0.0, 1.0)],
3992            vec![Vec4::new(0.0, 0.0, 1.0, 1.0), Vec4::new(1.0, 1.0, 1.0, 1.0)],
3993        ]);
3994        figure.add_surface_plot(surface);
3995
3996        let rebuilt = FigureScene::capture(&figure)
3997            .into_figure()
3998            .expect("scene restore should succeed");
3999        let PlotElement::Surface(surface) = rebuilt.plots().next().unwrap() else {
4000            panic!("expected surface")
4001        };
4002        assert!(surface.flatten_z);
4003        assert!(surface.image_mode);
4004        assert!(surface.color_grid.is_some());
4005        assert_eq!(
4006            surface.color_grid.as_ref().unwrap()[0][0],
4007            Vec4::new(1.0, 0.0, 0.0, 1.0)
4008        );
4009    }
4010
4011    #[test]
4012    fn figure_scene_roundtrip_preserves_area_lower_curve() {
4013        let mut figure = Figure::new();
4014        let area = AreaPlot::new(vec![1.0, 2.0], vec![2.0, 3.0])
4015            .unwrap()
4016            .with_lower_curve(vec![0.5, 1.0])
4017            .with_label("Stacked");
4018        figure.add_area_plot(area);
4019
4020        let rebuilt = FigureScene::capture(&figure)
4021            .into_figure()
4022            .expect("scene restore should succeed");
4023        let PlotElement::Area(area) = rebuilt.plots().next().unwrap() else {
4024            panic!("expected area")
4025        };
4026        assert_eq!(area.lower_y, Some(vec![0.5, 1.0]));
4027        assert_eq!(area.label.as_deref(), Some("Stacked"));
4028    }
4029
4030    #[test]
4031    fn figure_scene_roundtrip_preserves_axes_local_limits_and_colormap_state() {
4032        let mut figure = Figure::new().with_subplot_grid(1, 2);
4033        figure.set_axes_limits(1, Some((1.0, 2.0)), Some((3.0, 4.0)));
4034        figure.set_axes_z_limits(1, Some((5.0, 6.0)));
4035        figure.set_axes_grid_enabled(1, false);
4036        figure.set_axes_minor_grid_enabled(1, true);
4037        figure.set_axes_box_enabled(1, false);
4038        figure.set_axes_axis_equal(1, true);
4039        figure.set_axes_kind(1, AxesKind::Polar);
4040        figure.set_axes_colorbar_enabled(1, true);
4041        figure.set_axes_colormap(1, ColorMap::Hot);
4042        figure.set_axes_color_limits(1, Some((0.0, 10.0)));
4043        figure.set_axes_style(
4044            1,
4045            TextStyle {
4046                font_size: Some(14.0),
4047                ..Default::default()
4048            },
4049        );
4050        figure.set_active_axes_index(1);
4051
4052        let rebuilt = FigureScene::capture(&figure)
4053            .into_figure()
4054            .expect("scene restore should succeed");
4055        let meta = rebuilt.axes_metadata(1).unwrap();
4056        assert_eq!(meta.x_limits, Some((1.0, 2.0)));
4057        assert_eq!(meta.y_limits, Some((3.0, 4.0)));
4058        assert_eq!(meta.z_limits, Some((5.0, 6.0)));
4059        assert!(!meta.grid_enabled);
4060        assert!(meta.minor_grid_enabled);
4061        assert!(meta.minor_grid_explicit);
4062        assert!(!meta.box_enabled);
4063        assert!(meta.axis_equal);
4064        assert_eq!(meta.axes_kind, AxesKind::Polar);
4065        assert!(meta.colorbar_enabled);
4066        assert_eq!(format!("{:?}", meta.colormap), "Hot");
4067        assert_eq!(meta.color_limits, Some((0.0, 10.0)));
4068        assert_eq!(meta.axes_style.font_size, Some(14.0));
4069    }
4070
4071    #[test]
4072    fn axes_metadata_deserializes_without_axes_style() {
4073        let json = r#"{
4074            "legendEnabled": true,
4075            "colormap": "Parula",
4076            "titleStyle": {"visible": true},
4077            "xLabelStyle": {"visible": true},
4078            "yLabelStyle": {"visible": true},
4079            "zLabelStyle": {"visible": true},
4080            "legendStyle": {"visible": true}
4081        }"#;
4082        let serialized: SerializedAxesMetadata = serde_json::from_str(json).unwrap();
4083        let metadata = AxesMetadata::from(serialized);
4084        assert!(metadata.axes_style.color.is_none());
4085        assert!(metadata.axes_style.font_size.is_none());
4086        assert!(metadata.axes_style.font_weight.is_none());
4087        assert!(metadata.axes_style.font_angle.is_none());
4088        assert!(metadata.axes_style.interpreter.is_none());
4089        assert!(metadata.axes_style.visible);
4090    }
4091
4092    #[test]
4093    fn figure_scene_roundtrip_preserves_axes_local_annotation_metadata() {
4094        let mut figure = Figure::new().with_subplot_grid(1, 2);
4095        figure.set_sg_title("All Panels");
4096        figure.set_sg_title_style(TextStyle {
4097            font_weight: Some("bold".into()),
4098            font_size: Some(20.0),
4099            ..Default::default()
4100        });
4101        figure.set_active_axes_index(0);
4102        figure.set_axes_title(0, "Left");
4103        figure.set_axes_xlabel(0, "LX");
4104        figure.set_axes_ylabel(0, "LY");
4105        figure.set_axes_legend_enabled(0, false);
4106        figure.set_axes_title(1, "Right");
4107        figure.set_axes_xlabel(1, "RX");
4108        figure.set_axes_ylabel(1, "RY");
4109        figure.set_axes_legend_enabled(1, true);
4110        figure.set_axes_legend_style(
4111            1,
4112            LegendStyle {
4113                location: Some("northeast".into()),
4114                font_weight: Some("bold".into()),
4115                orientation: Some("horizontal".into()),
4116                ..Default::default()
4117            },
4118        );
4119        if let Some(meta) = figure.axes_metadata.get_mut(0) {
4120            meta.title_style.font_weight = Some("bold".into());
4121            meta.title_style.font_angle = Some("italic".into());
4122        }
4123        figure.set_active_axes_index(1);
4124
4125        let rebuilt = FigureScene::capture(&figure)
4126            .into_figure()
4127            .expect("scene restore should succeed");
4128
4129        assert_eq!(rebuilt.active_axes_index, 1);
4130        assert_eq!(rebuilt.sg_title.as_deref(), Some("All Panels"));
4131        assert_eq!(rebuilt.sg_title_style.font_weight.as_deref(), Some("bold"));
4132        assert_eq!(rebuilt.sg_title_style.font_size, Some(20.0));
4133        assert_eq!(
4134            rebuilt.axes_metadata(0).and_then(|m| m.title.as_deref()),
4135            Some("Left")
4136        );
4137        assert_eq!(
4138            rebuilt.axes_metadata(0).and_then(|m| m.x_label.as_deref()),
4139            Some("LX")
4140        );
4141        assert_eq!(
4142            rebuilt.axes_metadata(0).and_then(|m| m.y_label.as_deref()),
4143            Some("LY")
4144        );
4145        assert!(!rebuilt.axes_metadata(0).unwrap().legend_enabled);
4146        assert_eq!(
4147            rebuilt
4148                .axes_metadata(0)
4149                .unwrap()
4150                .title_style
4151                .font_weight
4152                .as_deref(),
4153            Some("bold")
4154        );
4155        assert_eq!(
4156            rebuilt
4157                .axes_metadata(0)
4158                .unwrap()
4159                .title_style
4160                .font_angle
4161                .as_deref(),
4162            Some("italic")
4163        );
4164        assert_eq!(
4165            rebuilt.axes_metadata(1).and_then(|m| m.title.as_deref()),
4166            Some("Right")
4167        );
4168        assert_eq!(
4169            rebuilt.axes_metadata(1).and_then(|m| m.x_label.as_deref()),
4170            Some("RX")
4171        );
4172        assert_eq!(
4173            rebuilt.axes_metadata(1).and_then(|m| m.y_label.as_deref()),
4174            Some("RY")
4175        );
4176        assert_eq!(
4177            rebuilt
4178                .axes_metadata(1)
4179                .unwrap()
4180                .legend_style
4181                .location
4182                .as_deref(),
4183            Some("northeast")
4184        );
4185        assert_eq!(
4186            rebuilt
4187                .axes_metadata(1)
4188                .unwrap()
4189                .legend_style
4190                .font_weight
4191                .as_deref(),
4192            Some("bold")
4193        );
4194        assert_eq!(
4195            rebuilt
4196                .axes_metadata(1)
4197                .unwrap()
4198                .legend_style
4199                .orientation
4200                .as_deref(),
4201            Some("horizontal")
4202        );
4203    }
4204
4205    #[test]
4206    fn figure_scene_roundtrip_preserves_axes_local_log_modes() {
4207        let mut figure = Figure::new().with_subplot_grid(1, 2);
4208        figure.set_axes_log_modes(0, true, false);
4209        figure.set_axes_log_modes(1, false, true);
4210        figure.set_active_axes_index(1);
4211
4212        let rebuilt = FigureScene::capture(&figure)
4213            .into_figure()
4214            .expect("scene restore should succeed");
4215
4216        assert!(rebuilt.axes_metadata(0).unwrap().x_log);
4217        assert!(!rebuilt.axes_metadata(0).unwrap().y_log);
4218        assert!(!rebuilt.axes_metadata(1).unwrap().x_log);
4219        assert!(rebuilt.axes_metadata(1).unwrap().y_log);
4220        assert!(!rebuilt.x_log);
4221        assert!(rebuilt.y_log);
4222    }
4223
4224    #[test]
4225    fn figure_scene_roundtrip_preserves_zlabel_and_view_state() {
4226        let mut figure = Figure::new().with_subplot_grid(1, 2);
4227        figure.set_axes_zlabel(1, "Height");
4228        figure.set_axes_view(1, 45.0, 20.0);
4229        figure.set_active_axes_index(1);
4230
4231        let rebuilt = FigureScene::capture(&figure)
4232            .into_figure()
4233            .expect("scene restore should succeed");
4234
4235        assert_eq!(
4236            rebuilt.axes_metadata(1).unwrap().z_label.as_deref(),
4237            Some("Height")
4238        );
4239        assert_eq!(
4240            rebuilt.axes_metadata(1).unwrap().view_azimuth_deg,
4241            Some(45.0)
4242        );
4243        assert_eq!(
4244            rebuilt.axes_metadata(1).unwrap().view_elevation_deg,
4245            Some(20.0)
4246        );
4247        assert_eq!(rebuilt.z_label.as_deref(), Some("Height"));
4248    }
4249
4250    #[test]
4251    fn figure_scene_roundtrip_preserves_pie_metadata() {
4252        let mut figure = Figure::new();
4253        let pie = crate::plots::PieChart::new(vec![1.0, 2.0], None)
4254            .unwrap()
4255            .with_slice_labels(vec!["A".into(), "B".into()])
4256            .with_explode(vec![false, true]);
4257        figure.add_pie_chart(pie);
4258
4259        let rebuilt = FigureScene::capture(&figure)
4260            .into_figure()
4261            .expect("scene restore should succeed");
4262        let crate::plots::figure::PlotElement::Pie(pie) = rebuilt.plots().next().unwrap() else {
4263            panic!("expected pie")
4264        };
4265        assert_eq!(pie.slice_labels, vec!["A", "B"]);
4266        assert_eq!(pie.explode, vec![false, true]);
4267    }
4268
4269    #[test]
4270    fn scene_plot_deserialize_maps_null_numeric_values_to_nan() {
4271        let json = r#"{
4272          "schemaVersion": 1,
4273          "layout": { "axesRows": 1, "axesCols": 1, "axesIndices": [0] },
4274          "metadata": {
4275            "gridEnabled": true,
4276            "legendEnabled": false,
4277            "colorbarEnabled": false,
4278            "axisEqual": false,
4279            "backgroundRgba": [1,1,1,1],
4280            "legendEntries": []
4281          },
4282          "plots": [
4283            {
4284              "kind": "surface",
4285              "x": [0.0, null],
4286              "y": [0.0, 1.0],
4287              "z": [[0.0, null], [1.0, 2.0]],
4288              "colormap": "Parula",
4289              "shading_mode": "Smooth",
4290              "wireframe": false,
4291              "alpha": 1.0,
4292              "flatten_z": false,
4293              "color_limits": null,
4294              "axes_index": 0,
4295              "label": null,
4296              "visible": true
4297            }
4298          ]
4299        }"#;
4300        let scene: FigureScene = serde_json::from_str(json).expect("scene should deserialize");
4301        let ScenePlot::Surface { x, z, .. } = &scene.plots[0] else {
4302            panic!("expected surface plot");
4303        };
4304        assert!(x[1].is_nan());
4305        assert!(z[0][1].is_nan());
4306    }
4307
4308    #[test]
4309    fn scene_plot_deserialize_maps_null_scatter3_components_to_nan() {
4310        let json = r#"{
4311          "schemaVersion": 1,
4312          "layout": { "axesRows": 1, "axesCols": 1, "axesIndices": [0] },
4313          "metadata": {
4314            "gridEnabled": true,
4315            "legendEnabled": false,
4316            "colorbarEnabled": false,
4317            "axisEqual": false,
4318            "backgroundRgba": [1,1,1,1],
4319            "legendEntries": []
4320          },
4321          "plots": [
4322            {
4323              "kind": "scatter3",
4324              "points": [[0.0, 1.0, null], [1.0, null, 2.0]],
4325              "colors_rgba": [[0.2, 0.4, 0.6, 1.0], [0.1, 0.2, 0.3, 1.0]],
4326              "point_size": 6.0,
4327              "point_sizes": [3.0, null],
4328              "axes_index": 0,
4329              "label": null,
4330              "visible": true
4331            }
4332          ]
4333        }"#;
4334        let scene: FigureScene = serde_json::from_str(json).expect("scene should deserialize");
4335        let ScenePlot::Scatter3 {
4336            points,
4337            point_sizes,
4338            ..
4339        } = &scene.plots[0]
4340        else {
4341            panic!("expected scatter3 plot");
4342        };
4343        assert!(points[0][2].is_nan());
4344        assert!(points[1][1].is_nan());
4345        assert!(point_sizes.as_ref().unwrap()[1].is_nan());
4346    }
4347}