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};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct FigureEvent {
17 pub handle: u32,
18 pub kind: FigureEventKind,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub fingerprint: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub figure: Option<FigureSnapshot>,
23}
24
25#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(rename_all = "lowercase")]
28pub enum FigureEventKind {
29 Created,
30 Updated,
31 Cleared,
32 Closed,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct FigureSnapshot {
39 pub layout: FigureLayout,
40 pub metadata: FigureMetadata,
41 pub plots: Vec<PlotDescriptor>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct FigureScene {
48 pub schema_version: u32,
49 pub layout: FigureLayout,
50 pub metadata: FigureMetadata,
51 pub plots: Vec<ScenePlot>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(tag = "kind", rename_all = "snake_case")]
56pub enum ScenePlot {
57 Line {
58 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
59 x: Vec<f64>,
60 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
61 y: Vec<f64>,
62 color_rgba: [f32; 4],
63 line_width: f32,
64 line_style: String,
65 axes_index: u32,
66 label: Option<String>,
67 visible: bool,
68 },
69 ReferenceLine {
70 orientation: String,
71 #[serde(deserialize_with = "deserialize_f64_lossy")]
72 value: f64,
73 color_rgba: [f32; 4],
74 line_width: f32,
75 line_style: String,
76 label: Option<String>,
77 display_name: Option<String>,
78 label_orientation: String,
79 axes_index: u32,
80 visible: bool,
81 },
82 Scatter {
83 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
84 x: Vec<f64>,
85 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
86 y: Vec<f64>,
87 color_rgba: [f32; 4],
88 marker_size: f32,
89 marker_style: String,
90 axes_index: u32,
91 label: Option<String>,
92 visible: bool,
93 },
94 Bar {
95 labels: Vec<String>,
96 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
97 values: Vec<f64>,
98 #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
99 histogram_bin_edges: Option<Vec<f64>>,
100 color_rgba: [f32; 4],
101 #[serde(default)]
102 outline_color_rgba: Option<[f32; 4]>,
103 bar_width: f32,
104 outline_width: f32,
105 orientation: String,
106 group_index: u32,
107 group_count: u32,
108 #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
109 stack_offsets: Option<Vec<f64>>,
110 axes_index: u32,
111 label: Option<String>,
112 visible: bool,
113 },
114 ErrorBar {
115 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
116 x: Vec<f64>,
117 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
118 y: Vec<f64>,
119 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
120 err_low: Vec<f64>,
121 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
122 err_high: Vec<f64>,
123 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
124 x_err_low: Vec<f64>,
125 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
126 x_err_high: Vec<f64>,
127 orientation: String,
128 color_rgba: [f32; 4],
129 line_width: f32,
130 line_style: String,
131 cap_width: f32,
132 marker_style: Option<String>,
133 marker_size: Option<f32>,
134 marker_face_color: Option<[f32; 4]>,
135 marker_edge_color: Option<[f32; 4]>,
136 marker_filled: Option<bool>,
137 axes_index: u32,
138 label: Option<String>,
139 visible: bool,
140 },
141 Stairs {
142 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
143 x: Vec<f64>,
144 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
145 y: Vec<f64>,
146 color_rgba: [f32; 4],
147 line_width: f32,
148 axes_index: u32,
149 label: Option<String>,
150 visible: bool,
151 },
152 Stem {
153 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
154 x: Vec<f64>,
155 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
156 y: Vec<f64>,
157 #[serde(deserialize_with = "deserialize_f64_lossy")]
158 baseline: f64,
159 color_rgba: [f32; 4],
160 line_width: f32,
161 line_style: String,
162 baseline_color_rgba: [f32; 4],
163 baseline_visible: bool,
164 marker_color_rgba: [f32; 4],
165 marker_size: f32,
166 marker_filled: bool,
167 axes_index: u32,
168 label: Option<String>,
169 visible: bool,
170 },
171 Area {
172 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
173 x: Vec<f64>,
174 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
175 y: Vec<f64>,
176 #[serde(default, deserialize_with = "deserialize_option_vec_f64_lossy")]
177 lower_y: Option<Vec<f64>>,
178 #[serde(deserialize_with = "deserialize_f64_lossy")]
179 baseline: f64,
180 color_rgba: [f32; 4],
181 axes_index: u32,
182 label: Option<String>,
183 visible: bool,
184 },
185 Quiver {
186 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
187 x: Vec<f64>,
188 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
189 y: Vec<f64>,
190 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
191 u: Vec<f64>,
192 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
193 v: Vec<f64>,
194 color_rgba: [f32; 4],
195 line_width: f32,
196 scale: f32,
197 head_size: f32,
198 axes_index: u32,
199 label: Option<String>,
200 visible: bool,
201 },
202 Surface {
203 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
204 x: Vec<f64>,
205 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
206 y: Vec<f64>,
207 #[serde(deserialize_with = "deserialize_matrix_f64_lossy")]
208 z: Vec<Vec<f64>>,
209 colormap: String,
210 shading_mode: String,
211 wireframe: bool,
212 alpha: f32,
213 flatten_z: bool,
214 #[serde(default)]
215 image_mode: bool,
216 #[serde(default)]
217 color_grid_rgba: Option<Vec<Vec<[f32; 4]>>>,
218 #[serde(default, deserialize_with = "deserialize_option_pair_f64_lossy")]
219 color_limits: Option<[f64; 2]>,
220 axes_index: u32,
221 label: Option<String>,
222 visible: bool,
223 },
224 Patch {
225 #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
226 vertices: Vec<[f32; 3]>,
227 faces: Vec<Vec<u32>>,
228 face_color_rgba: [f32; 4],
229 edge_color_rgba: [f32; 4],
230 face_color_mode: String,
231 edge_color_mode: String,
232 face_alpha: f32,
233 edge_alpha: f32,
234 line_width: f32,
235 axes_index: u32,
236 label: Option<String>,
237 visible: bool,
238 #[serde(default)]
239 force_3d: bool,
240 },
241 Mesh {
242 #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
243 vertices: Vec<[f32; 3]>,
244 triangles: Vec<[u32; 3]>,
245 mesh_id: Option<String>,
246 face_color_rgba: [f32; 4],
247 edge_color_rgba: [f32; 4],
248 face_alpha: f32,
249 edge_alpha: f32,
250 edge_width: f32,
251 #[serde(default)]
252 edge_mode: String,
253 #[serde(default, skip_serializing_if = "Vec::is_empty")]
254 feature_edge_groups: Vec<u64>,
255 #[serde(default, skip_serializing_if = "Vec::is_empty")]
256 vertex_colors_rgba: Vec<[f32; 4]>,
257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
258 triangle_colors_rgba: Vec<[f32; 4]>,
259 axes_index: u32,
260 label: Option<String>,
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
262 regions: Vec<SerializedMeshRegion>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 highlighted_region_id: Option<String>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 highlight_color_rgba: Option<[f32; 4]>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 scalar_field: Option<Box<SerializedMeshScalarField>>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 vector_field: Option<Box<SerializedMeshVectorField>>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 deformation: Option<Box<SerializedMeshDeformation>>,
273 visible: bool,
274 },
275 Line3 {
276 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
277 x: Vec<f64>,
278 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
279 y: Vec<f64>,
280 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
281 z: Vec<f64>,
282 color_rgba: [f32; 4],
283 line_width: f32,
284 line_style: String,
285 axes_index: u32,
286 label: Option<String>,
287 visible: bool,
288 },
289 Scatter3 {
290 #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
291 points: Vec<[f32; 3]>,
292 #[serde(default, deserialize_with = "deserialize_vec_rgba_f32_lossy")]
293 colors_rgba: Vec<[f32; 4]>,
294 point_size: f32,
295 #[serde(default, deserialize_with = "deserialize_option_vec_f32_lossy")]
296 point_sizes: Option<Vec<f32>>,
297 axes_index: u32,
298 label: Option<String>,
299 visible: bool,
300 },
301 Contour {
302 vertices: Vec<SerializedVertex>,
303 bounds_min: [f32; 3],
304 bounds_max: [f32; 3],
305 base_z: f32,
306 line_width: f32,
307 axes_index: u32,
308 label: Option<String>,
309 visible: bool,
310 #[serde(default)]
311 force_3d: bool,
312 },
313 ContourFill {
314 vertices: Vec<SerializedVertex>,
315 bounds_min: [f32; 3],
316 bounds_max: [f32; 3],
317 axes_index: u32,
318 label: Option<String>,
319 visible: bool,
320 },
321 Pie {
322 #[serde(deserialize_with = "deserialize_vec_f64_lossy")]
323 values: Vec<f64>,
324 colors_rgba: Vec<[f32; 4]>,
325 slice_labels: Vec<String>,
326 label_format: Option<String>,
327 explode: Vec<bool>,
328 axes_index: u32,
329 label: Option<String>,
330 visible: bool,
331 },
332 Unsupported {
333 plot_kind: PlotKind,
334 axes_index: u32,
335 label: Option<String>,
336 visible: bool,
337 },
338}
339
340impl FigureSnapshot {
341 pub fn capture(figure: &Figure) -> Self {
343 let (rows, cols) = figure.axes_grid();
344 let layout = FigureLayout {
345 axes_rows: rows as u32,
346 axes_cols: cols as u32,
347 axes_indices: figure
348 .plot_axes_indices()
349 .iter()
350 .map(|idx| *idx as u32)
351 .collect(),
352 };
353
354 let metadata = FigureMetadata::from_figure(figure);
355
356 let plots = figure
357 .plots()
358 .enumerate()
359 .map(|(idx, plot)| PlotDescriptor::from_plot(plot, figure_axis_index(figure, idx)))
360 .collect();
361
362 Self {
363 layout,
364 metadata,
365 plots,
366 }
367 }
368
369 pub fn fingerprint(&self) -> String {
370 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
371 const FNV_PRIME: u64 = 0x100000001b3;
372
373 let bytes = serde_json::to_vec(self).unwrap_or_default();
374 let mut hash = FNV_OFFSET_BASIS;
375 for byte in bytes {
376 hash ^= u64::from(byte);
377 hash = hash.wrapping_mul(FNV_PRIME);
378 }
379 format!("fig:{hash:016x}")
380 }
381}
382
383impl FigureScene {
384 pub const SCHEMA_VERSION: u32 = 3;
385
386 pub fn capture(figure: &Figure) -> Self {
387 let snapshot = FigureSnapshot::capture(figure);
388 let plots = figure
389 .plots()
390 .enumerate()
391 .map(|(idx, plot)| ScenePlot::from_plot(plot, figure_axis_index(figure, idx)))
392 .collect();
393
394 Self {
395 schema_version: Self::SCHEMA_VERSION,
396 layout: snapshot.layout,
397 metadata: snapshot.metadata,
398 plots,
399 }
400 }
401
402 pub fn from_geometry_scene(scene: &crate::geometry_scene::GeometryScene) -> Self {
403 let mut figure = Figure::new()
404 .with_grid(scene.show_grid)
405 .with_legend(false)
406 .with_axis_equal(scene.axis_equal);
407 figure.title = scene.title.clone();
408 figure.x_label = Some("X".to_string());
409 figure.y_label = Some("Y".to_string());
410 figure.z_label = Some("Z".to_string());
411 figure.set_axes_view(0, -38.0, 24.0);
412 let snapshot = FigureSnapshot::capture(&figure);
413 let plots = scene
414 .chunks
415 .iter()
416 .filter_map(scene_chunk_to_mesh_plot)
417 .collect::<Vec<_>>();
418
419 Self {
420 schema_version: Self::SCHEMA_VERSION,
421 layout: FigureLayout {
422 axes_rows: 1,
423 axes_cols: 1,
424 axes_indices: vec![0; plots.len()],
425 },
426 metadata: snapshot.metadata,
427 plots,
428 }
429 }
430
431 pub fn into_figure(self) -> Result<Figure, String> {
432 self.validate_schema_version()?;
433
434 let mut figure = Figure::new();
435 figure.set_subplot_grid(
436 self.layout.axes_rows as usize,
437 self.layout.axes_cols as usize,
438 );
439 figure.active_axes_index = self.metadata.active_axes_index as usize;
440 if let Some(axes_metadata) = self.metadata.axes_metadata.clone() {
441 figure.axes_metadata = axes_metadata.into_iter().map(AxesMetadata::from).collect();
442 figure.set_active_axes_index(figure.active_axes_index);
443 } else {
444 figure.title = self.metadata.title;
445 figure.x_label = self.metadata.x_label;
446 figure.y_label = self.metadata.y_label;
447 figure.legend_enabled = self.metadata.legend_enabled;
448 }
449 figure.name = self.metadata.name;
450 figure.number_title = self.metadata.number_title;
451 figure.visible = self.metadata.visible;
452 figure.sg_title = self.metadata.sg_title;
453 figure.sg_title_style = self
454 .metadata
455 .sg_title_style
456 .map(TextStyle::from)
457 .unwrap_or_default();
458 figure.grid_enabled = self.metadata.grid_enabled;
459 figure.minor_grid_enabled = self.metadata.minor_grid_enabled;
460 figure.z_limits = self.metadata.z_limits.map(|[lo, hi]| (lo, hi));
461 figure.colorbar_enabled = self.metadata.colorbar_enabled;
462 figure.axis_equal = self.metadata.axis_equal;
463 figure.background_color = rgba_to_vec4(self.metadata.background_rgba);
464
465 for plot in self.plots {
466 plot.apply_to_figure(&mut figure)?;
467 }
468
469 Ok(figure)
470 }
471
472 pub fn into_geometry_scene(
473 self,
474 scene_id: impl Into<String>,
475 revision: u64,
476 ) -> Result<crate::GeometryScene, String> {
477 self.validate_schema_version()?;
478 let scene_id = scene_id.into();
479 let mut chunks = Vec::new();
480 for (plot_index, plot) in self.plots.into_iter().enumerate() {
481 append_geometry_scene_chunks(&scene_id, plot_index, plot, &mut chunks)?;
482 }
483 if chunks.is_empty() {
484 return Err("figure scene does not contain renderable mesh plots".to_string());
485 }
486 let mut scene = crate::GeometryScene::new(scene_id, revision, chunks).with_title(
487 self.metadata
488 .title
489 .unwrap_or_else(|| "Geometry Preview".to_string()),
490 );
491 scene.show_grid = self.metadata.grid_enabled;
492 scene.axis_equal = self.metadata.axis_equal;
493 Ok(scene)
494 }
495
496 fn validate_schema_version(&self) -> Result<(), String> {
497 if self.schema_version == 0 || self.schema_version > FigureScene::SCHEMA_VERSION {
498 return Err(format!(
499 "unsupported figure scene schema version {} (supported 1..={})",
500 self.schema_version,
501 FigureScene::SCHEMA_VERSION
502 ));
503 }
504 if self.schema_version < 2
505 && self
506 .plots
507 .iter()
508 .any(|plot| matches!(plot, ScenePlot::Patch { .. }))
509 {
510 return Err(format!(
511 "patch plots require figure scene schema version {}",
512 2
513 ));
514 }
515 if self.schema_version < 3
516 && self
517 .plots
518 .iter()
519 .any(|plot| matches!(plot, ScenePlot::Mesh { .. }))
520 {
521 return Err(format!(
522 "mesh plots require figure scene schema version {}",
523 3
524 ));
525 }
526 Ok(())
527 }
528}
529
530fn append_geometry_scene_chunks(
531 scene_id: &str,
532 plot_index: usize,
533 plot: ScenePlot,
534 chunks: &mut Vec<crate::GeometrySceneChunk>,
535) -> Result<(), String> {
536 let ScenePlot::Mesh {
537 vertices,
538 triangles,
539 mesh_id,
540 face_color_rgba,
541 edge_color_rgba,
542 face_alpha,
543 edge_alpha,
544 edge_width,
545 edge_mode,
546 feature_edge_groups,
547 vertex_colors_rgba,
548 triangle_colors_rgba,
549 axes_index: _,
550 label,
551 regions,
552 highlighted_region_id,
553 highlight_color_rgba,
554 scalar_field,
555 vector_field,
556 deformation,
557 visible,
558 } = plot
559 else {
560 return Ok(());
561 };
562
563 if !visible {
564 return Ok(());
565 }
566
567 let region_metadata = regions
568 .iter()
569 .cloned()
570 .map(crate::geometry_scene::GeometrySceneRegion::from)
571 .collect::<Vec<_>>();
572 let mesh_id_for_chunk = mesh_id
573 .clone()
574 .unwrap_or_else(|| format!("mesh_{}", plot_index + 1));
575 let mut mesh = MeshPlot::new(vertices.into_iter().map(xyz_to_vec3).collect(), triangles)?;
576 mesh.set_mesh_id(mesh_id.clone());
577 mesh.set_face_color(rgba_to_vec4(face_color_rgba));
578 mesh.set_edge_color(rgba_to_vec4(edge_color_rgba));
579 mesh.set_face_alpha(face_alpha);
580 mesh.set_edge_alpha(edge_alpha);
581 mesh.set_edge_width(edge_width);
582 mesh.set_edge_mode(parse_mesh_edge_mode(&edge_mode));
583 if !feature_edge_groups.is_empty() {
584 mesh.set_feature_edge_groups(Some(feature_edge_groups))?;
585 }
586 if !vertex_colors_rgba.is_empty() {
587 mesh.set_vertex_colors(Some(
588 vertex_colors_rgba.into_iter().map(rgba_to_vec4).collect(),
589 ))?;
590 }
591 if !triangle_colors_rgba.is_empty() {
592 mesh.set_triangle_colors(Some(
593 triangle_colors_rgba.into_iter().map(rgba_to_vec4).collect(),
594 ))?;
595 }
596 mesh.set_label(label.clone());
597 mesh.set_regions(regions.into_iter().map(Into::into).collect());
598 mesh.set_highlighted_region_id(highlighted_region_id);
599 if let Some(color) = highlight_color_rgba {
600 mesh.set_highlight_color(rgba_to_vec4(color));
601 }
602 if let Some(field) = scalar_field {
603 mesh.set_scalar_field(Some((*field).try_into()?))?;
604 }
605 if let Some(field) = vector_field {
606 mesh.set_vector_field(Some((*field).try_into()?))?;
607 }
608 if let Some(field) = deformation {
609 mesh.set_deformation(Some((*field).into()))?;
610 }
611
612 let face_render_data = mesh.render_data();
613 chunks.push(
614 crate::GeometrySceneChunk::from_render_data(
615 format!("{scene_id}:{mesh_id_for_chunk}:faces:{plot_index}"),
616 face_render_data,
617 )
618 .with_mesh_id(mesh_id_for_chunk.clone())
619 .with_label(label.clone().unwrap_or_else(|| mesh_id_for_chunk.clone()))
620 .with_regions(region_metadata),
621 );
622
623 if let Some(edge_render_data) = mesh.edge_render_data() {
624 chunks.push(
625 crate::GeometrySceneChunk::from_render_data(
626 format!("{scene_id}:{mesh_id_for_chunk}:edges:{plot_index}"),
627 edge_render_data,
628 )
629 .with_mesh_id(mesh_id_for_chunk.clone())
630 .with_label(format!(
631 "{} edges",
632 label.clone().unwrap_or_else(|| mesh_id_for_chunk.clone())
633 )),
634 );
635 }
636
637 if let Some(vector_render_data) = mesh.vector_render_data() {
638 chunks.push(
639 crate::GeometrySceneChunk::from_render_data(
640 format!("{scene_id}:{mesh_id_for_chunk}:vectors:{plot_index}"),
641 vector_render_data,
642 )
643 .with_mesh_id(mesh_id_for_chunk.clone())
644 .with_label(format!(
645 "{} vectors",
646 label.unwrap_or_else(|| mesh_id_for_chunk.clone())
647 )),
648 );
649 }
650
651 Ok(())
652}
653
654fn scene_chunk_to_mesh_plot(
655 chunk: &crate::geometry_scene::GeometrySceneChunk,
656) -> Option<ScenePlot> {
657 if chunk.render_data.pipeline_type != crate::core::PipelineType::Triangles {
658 return None;
659 }
660 let indices = chunk.indices.as_ref()?;
661 if indices.len() < 3 {
662 return None;
663 }
664 let triangles = indices
665 .chunks_exact(3)
666 .map(|item| [item[0], item[1], item[2]])
667 .collect::<Vec<_>>();
668 if triangles.is_empty() {
669 return None;
670 }
671 let vertices = chunk
672 .vertices
673 .iter()
674 .map(|vertex| vertex.position)
675 .collect::<Vec<_>>();
676 Some(ScenePlot::Mesh {
677 vertices,
678 triangles,
679 mesh_id: chunk.mesh_id.clone(),
680 face_color_rgba: chunk.material.albedo.to_array(),
681 edge_color_rgba: [0.08, 0.10, 0.13, 1.0],
682 face_alpha: chunk.material.albedo.w,
683 edge_alpha: 0.0,
684 edge_width: 0.0,
685 edge_mode: "none".to_string(),
686 feature_edge_groups: Vec::new(),
687 vertex_colors_rgba: Vec::new(),
688 triangle_colors_rgba: Vec::new(),
689 axes_index: 0,
690 label: chunk.label.clone(),
691 regions: chunk.regions.iter().map(Into::into).collect(),
692 highlighted_region_id: None,
693 highlight_color_rgba: Some([0.98, 0.78, 0.22, 1.0]),
694 scalar_field: None,
695 vector_field: None,
696 deformation: None,
697 visible: chunk.visible,
698 })
699}
700
701fn figure_axis_index(figure: &Figure, plot_index: usize) -> u32 {
702 figure
703 .plot_axes_indices()
704 .get(plot_index)
705 .copied()
706 .unwrap_or(0) as u32
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
711#[serde(rename_all = "camelCase")]
712pub struct FigureLayout {
713 pub axes_rows: u32,
714 pub axes_cols: u32,
715 pub axes_indices: Vec<u32>,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize)]
720#[serde(rename_all = "camelCase")]
721pub struct FigureMetadata {
722 #[serde(skip_serializing_if = "Option::is_none")]
723 pub name: Option<String>,
724 #[serde(default = "default_true", skip_serializing_if = "is_true")]
725 pub number_title: bool,
726 #[serde(default = "default_true", skip_serializing_if = "is_true")]
727 pub visible: bool,
728 #[serde(skip_serializing_if = "Option::is_none")]
729 pub title: Option<String>,
730 #[serde(skip_serializing_if = "Option::is_none")]
731 pub sg_title: Option<String>,
732 #[serde(skip_serializing_if = "Option::is_none")]
733 pub sg_title_style: Option<SerializedTextStyle>,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub x_label: Option<String>,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 pub y_label: Option<String>,
738 pub grid_enabled: bool,
739 #[serde(default)]
740 pub minor_grid_enabled: bool,
741 pub legend_enabled: bool,
742 pub colorbar_enabled: bool,
743 pub axis_equal: bool,
744 pub background_rgba: [f32; 4],
745 #[serde(skip_serializing_if = "Option::is_none")]
746 pub colormap: Option<String>,
747 #[serde(skip_serializing_if = "Option::is_none")]
748 pub color_limits: Option<[f64; 2]>,
749 #[serde(skip_serializing_if = "Option::is_none")]
750 pub z_limits: Option<[f64; 2]>,
751 pub legend_entries: Vec<FigureLegendEntry>,
752 #[serde(default)]
753 pub active_axes_index: u32,
754 #[serde(skip_serializing_if = "Option::is_none")]
755 pub axes_metadata: Option<Vec<SerializedAxesMetadata>>,
756}
757
758impl FigureMetadata {
759 fn from_figure(figure: &Figure) -> Self {
760 let legend_entries = figure
761 .legend_entries()
762 .into_iter()
763 .map(FigureLegendEntry::from)
764 .collect();
765
766 Self {
767 name: figure.name.clone(),
768 number_title: figure.number_title,
769 visible: figure.visible,
770 title: figure.title.clone(),
771 sg_title: figure.sg_title.clone(),
772 sg_title_style: figure
773 .sg_title
774 .as_ref()
775 .map(|_| figure.sg_title_style.clone().into()),
776 x_label: figure.x_label.clone(),
777 y_label: figure.y_label.clone(),
778 grid_enabled: figure.grid_enabled,
779 minor_grid_enabled: figure.minor_grid_enabled,
780 legend_enabled: figure.legend_enabled,
781 colorbar_enabled: figure.colorbar_enabled,
782 axis_equal: figure.axis_equal,
783 background_rgba: vec4_to_rgba(figure.background_color),
784 colormap: Some(format!("{:?}", figure.colormap)),
785 color_limits: figure.color_limits.map(|(lo, hi)| [lo, hi]),
786 z_limits: figure.z_limits.map(|(lo, hi)| [lo, hi]),
787 legend_entries,
788 active_axes_index: figure.active_axes_index as u32,
789 axes_metadata: Some(
790 figure
791 .axes_metadata
792 .iter()
793 .cloned()
794 .map(SerializedAxesMetadata::from)
795 .collect(),
796 ),
797 }
798 }
799}
800
801fn default_true() -> bool {
802 true
803}
804
805fn is_true(value: &bool) -> bool {
806 *value
807}
808
809fn is_false(value: &bool) -> bool {
810 !*value
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize)]
814#[serde(rename_all = "camelCase")]
815pub struct SerializedTextStyle {
816 #[serde(skip_serializing_if = "Option::is_none")]
817 pub color_rgba: Option<[f32; 4]>,
818 #[serde(skip_serializing_if = "Option::is_none")]
819 pub font_size: Option<f32>,
820 #[serde(skip_serializing_if = "Option::is_none")]
821 pub font_weight: Option<String>,
822 #[serde(skip_serializing_if = "Option::is_none")]
823 pub font_angle: Option<String>,
824 #[serde(skip_serializing_if = "Option::is_none")]
825 pub interpreter: Option<String>,
826 pub visible: bool,
827}
828
829impl Default for SerializedTextStyle {
830 fn default() -> Self {
831 TextStyle::default().into()
832 }
833}
834
835impl From<TextStyle> for SerializedTextStyle {
836 fn from(value: TextStyle) -> Self {
837 Self {
838 color_rgba: value.color.map(vec4_to_rgba),
839 font_size: value.font_size,
840 font_weight: value.font_weight,
841 font_angle: value.font_angle,
842 interpreter: value.interpreter,
843 visible: value.visible,
844 }
845 }
846}
847
848impl From<SerializedTextStyle> for TextStyle {
849 fn from(value: SerializedTextStyle) -> Self {
850 Self {
851 color: value.color_rgba.map(rgba_to_vec4),
852 font_size: value.font_size,
853 font_weight: value.font_weight,
854 font_angle: value.font_angle,
855 interpreter: value.interpreter,
856 visible: value.visible,
857 }
858 }
859}
860
861#[derive(Debug, Clone, Serialize, Deserialize)]
862#[serde(rename_all = "camelCase")]
863pub struct SerializedLegendStyle {
864 #[serde(skip_serializing_if = "Option::is_none")]
865 pub location: Option<String>,
866 pub visible: bool,
867 #[serde(skip_serializing_if = "Option::is_none")]
868 pub font_size: Option<f32>,
869 #[serde(skip_serializing_if = "Option::is_none")]
870 pub font_weight: Option<String>,
871 #[serde(skip_serializing_if = "Option::is_none")]
872 pub font_angle: Option<String>,
873 #[serde(skip_serializing_if = "Option::is_none")]
874 pub interpreter: Option<String>,
875 #[serde(skip_serializing_if = "Option::is_none")]
876 pub box_visible: Option<bool>,
877 #[serde(skip_serializing_if = "Option::is_none")]
878 pub orientation: Option<String>,
879 #[serde(skip_serializing_if = "Option::is_none")]
880 pub text_color_rgba: Option<[f32; 4]>,
881}
882
883impl From<LegendStyle> for SerializedLegendStyle {
884 fn from(value: LegendStyle) -> Self {
885 Self {
886 location: value.location,
887 visible: value.visible,
888 font_size: value.font_size,
889 font_weight: value.font_weight,
890 font_angle: value.font_angle,
891 interpreter: value.interpreter,
892 box_visible: value.box_visible,
893 orientation: value.orientation,
894 text_color_rgba: value.text_color.map(vec4_to_rgba),
895 }
896 }
897}
898
899impl From<SerializedLegendStyle> for LegendStyle {
900 fn from(value: SerializedLegendStyle) -> Self {
901 Self {
902 location: value.location,
903 visible: value.visible,
904 font_size: value.font_size,
905 font_weight: value.font_weight,
906 font_angle: value.font_angle,
907 interpreter: value.interpreter,
908 box_visible: value.box_visible,
909 orientation: value.orientation,
910 text_color: value.text_color_rgba.map(rgba_to_vec4),
911 }
912 }
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
916#[serde(rename_all = "camelCase")]
917pub struct SerializedAxesMetadata {
918 #[serde(default, skip_serializing_if = "is_cartesian_axes_kind")]
919 pub axes_kind: SerializedAxesKind,
920 #[serde(skip_serializing_if = "Option::is_none")]
921 pub title: Option<String>,
922 #[serde(skip_serializing_if = "Option::is_none")]
923 pub x_label: Option<String>,
924 #[serde(skip_serializing_if = "Option::is_none")]
925 pub y_label: Option<String>,
926 #[serde(skip_serializing_if = "Option::is_none")]
927 pub z_label: Option<String>,
928 #[serde(default, skip_serializing_if = "Option::is_none")]
929 pub x_tick_labels: Option<Vec<String>>,
930 #[serde(default, skip_serializing_if = "Option::is_none")]
931 pub y_tick_labels: Option<Vec<String>>,
932 #[serde(skip_serializing_if = "Option::is_none")]
933 pub x_limits: Option<[f64; 2]>,
934 #[serde(skip_serializing_if = "Option::is_none")]
935 pub y_limits: Option<[f64; 2]>,
936 #[serde(skip_serializing_if = "Option::is_none")]
937 pub z_limits: Option<[f64; 2]>,
938 #[serde(default)]
939 pub x_log: bool,
940 #[serde(default)]
941 pub y_log: bool,
942 #[serde(skip_serializing_if = "Option::is_none")]
943 pub view_azimuth_deg: Option<f32>,
944 #[serde(skip_serializing_if = "Option::is_none")]
945 pub view_elevation_deg: Option<f32>,
946 #[serde(default)]
947 pub grid_enabled: bool,
948 #[serde(default)]
949 pub minor_grid_enabled: bool,
950 #[serde(default, skip_serializing_if = "is_false")]
951 pub minor_grid_explicit: bool,
952 #[serde(default)]
953 pub box_enabled: bool,
954 #[serde(default)]
955 pub axis_equal: bool,
956 pub legend_enabled: bool,
957 #[serde(default)]
958 pub colorbar_enabled: bool,
959 pub colormap: String,
960 #[serde(skip_serializing_if = "Option::is_none")]
961 pub color_limits: Option<[f64; 2]>,
962 #[serde(default)]
963 pub axes_style: SerializedTextStyle,
964 pub title_style: SerializedTextStyle,
965 pub x_label_style: SerializedTextStyle,
966 pub y_label_style: SerializedTextStyle,
967 pub z_label_style: SerializedTextStyle,
968 pub legend_style: SerializedLegendStyle,
969 #[serde(default, skip_serializing_if = "Vec::is_empty")]
970 pub world_text_annotations: Vec<SerializedTextAnnotation>,
971}
972
973#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
974#[serde(rename_all = "camelCase")]
975pub enum SerializedAxesKind {
976 #[default]
977 Cartesian,
978 Polar,
979}
980
981fn is_cartesian_axes_kind(value: &SerializedAxesKind) -> bool {
982 *value == SerializedAxesKind::Cartesian
983}
984
985impl From<AxesKind> for SerializedAxesKind {
986 fn from(value: AxesKind) -> Self {
987 match value {
988 AxesKind::Cartesian => Self::Cartesian,
989 AxesKind::Polar => Self::Polar,
990 }
991 }
992}
993
994impl From<SerializedAxesKind> for AxesKind {
995 fn from(value: SerializedAxesKind) -> Self {
996 match value {
997 SerializedAxesKind::Cartesian => Self::Cartesian,
998 SerializedAxesKind::Polar => Self::Polar,
999 }
1000 }
1001}
1002
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004#[serde(rename_all = "camelCase")]
1005pub struct SerializedTextAnnotation {
1006 pub position: [f32; 3],
1007 pub text: String,
1008 pub style: SerializedTextStyle,
1009}
1010
1011#[derive(Debug, Clone, Serialize, Deserialize)]
1012#[serde(rename_all = "camelCase")]
1013pub struct SerializedMeshRegion {
1014 pub region_id: String,
1015 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub label: Option<String>,
1017 #[serde(default, skip_serializing_if = "Option::is_none")]
1018 pub tag: Option<String>,
1019 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1020 pub triangle_ranges: Vec<SerializedMeshTriangleRange>,
1021}
1022
1023#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct SerializedMeshTriangleRange {
1026 pub start: u32,
1027 pub count: u32,
1028}
1029
1030#[derive(Debug, Clone, Serialize, Deserialize)]
1031#[serde(rename_all = "camelCase")]
1032pub struct SerializedMeshScalarField {
1033 pub field_id: String,
1034 #[serde(default, skip_serializing_if = "Option::is_none")]
1035 pub label: Option<String>,
1036 pub location: String,
1037 #[serde(deserialize_with = "deserialize_vec_f32_lossy")]
1038 pub values: Vec<f32>,
1039 #[serde(default, skip_serializing_if = "Option::is_none")]
1040 pub color_limits: Option<[f32; 2]>,
1041 pub colormap: String,
1042 pub alpha: f32,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1046#[serde(rename_all = "camelCase")]
1047pub struct SerializedMeshVectorField {
1048 pub field_id: String,
1049 #[serde(default, skip_serializing_if = "Option::is_none")]
1050 pub label: Option<String>,
1051 pub location: String,
1052 #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
1053 pub vectors: Vec<[f32; 3]>,
1054 pub scale: f32,
1055 pub stride: usize,
1056 pub color_rgba: [f32; 4],
1057}
1058
1059#[derive(Debug, Clone, Serialize, Deserialize)]
1060#[serde(rename_all = "camelCase")]
1061pub struct SerializedMeshDeformation {
1062 pub field_id: String,
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1064 pub label: Option<String>,
1065 #[serde(deserialize_with = "deserialize_vec_xyz_f32_lossy")]
1066 pub displacements: Vec<[f32; 3]>,
1067 pub scale: f32,
1068}
1069
1070impl From<AxesMetadata> for SerializedAxesMetadata {
1071 fn from(value: AxesMetadata) -> Self {
1072 Self {
1073 axes_kind: value.axes_kind.into(),
1074 title: value.title,
1075 x_label: value.x_label,
1076 y_label: value.y_label,
1077 z_label: value.z_label,
1078 x_tick_labels: value.x_tick_labels,
1079 y_tick_labels: value.y_tick_labels,
1080 x_limits: value.x_limits.map(|(a, b)| [a, b]),
1081 y_limits: value.y_limits.map(|(a, b)| [a, b]),
1082 z_limits: value.z_limits.map(|(a, b)| [a, b]),
1083 x_log: value.x_log,
1084 y_log: value.y_log,
1085 view_azimuth_deg: value.view_azimuth_deg,
1086 view_elevation_deg: value.view_elevation_deg,
1087 grid_enabled: value.grid_enabled,
1088 minor_grid_enabled: value.minor_grid_enabled,
1089 minor_grid_explicit: value.minor_grid_explicit,
1090 box_enabled: value.box_enabled,
1091 axis_equal: value.axis_equal,
1092 legend_enabled: value.legend_enabled,
1093 colorbar_enabled: value.colorbar_enabled,
1094 colormap: format!("{:?}", value.colormap),
1095 color_limits: value.color_limits.map(|(a, b)| [a, b]),
1096 axes_style: value.axes_style.into(),
1097 title_style: value.title_style.into(),
1098 x_label_style: value.x_label_style.into(),
1099 y_label_style: value.y_label_style.into(),
1100 z_label_style: value.z_label_style.into(),
1101 legend_style: value.legend_style.into(),
1102 world_text_annotations: value
1103 .world_text_annotations
1104 .into_iter()
1105 .map(Into::into)
1106 .collect(),
1107 }
1108 }
1109}
1110
1111impl From<SerializedAxesMetadata> for AxesMetadata {
1112 fn from(value: SerializedAxesMetadata) -> Self {
1113 Self {
1114 axes_kind: value.axes_kind.into(),
1115 title: value.title,
1116 x_label: value.x_label,
1117 y_label: value.y_label,
1118 z_label: value.z_label,
1119 x_tick_labels: value.x_tick_labels,
1120 y_tick_labels: value.y_tick_labels,
1121 x_limits: value.x_limits.map(|[a, b]| (a, b)),
1122 y_limits: value.y_limits.map(|[a, b]| (a, b)),
1123 z_limits: value.z_limits.map(|[a, b]| (a, b)),
1124 x_log: value.x_log,
1125 y_log: value.y_log,
1126 view_azimuth_deg: value.view_azimuth_deg,
1127 view_elevation_deg: value.view_elevation_deg,
1128 view_revision: 0,
1129 grid_enabled: value.grid_enabled,
1130 minor_grid_enabled: value.minor_grid_enabled,
1131 minor_grid_explicit: value.minor_grid_explicit || value.minor_grid_enabled,
1132 box_enabled: value.box_enabled,
1133 axis_equal: value.axis_equal,
1134 legend_enabled: value.legend_enabled,
1135 colorbar_enabled: value.colorbar_enabled,
1136 colormap: parse_colormap_name(&value.colormap),
1137 color_limits: value.color_limits.map(|[a, b]| (a, b)),
1138 axes_style: value.axes_style.into(),
1139 title_style: value.title_style.into(),
1140 x_label_style: value.x_label_style.into(),
1141 y_label_style: value.y_label_style.into(),
1142 z_label_style: value.z_label_style.into(),
1143 legend_style: value.legend_style.into(),
1144 world_text_annotations: value
1145 .world_text_annotations
1146 .into_iter()
1147 .map(Into::into)
1148 .collect(),
1149 }
1150 }
1151}
1152
1153impl From<crate::plots::figure::TextAnnotation> for SerializedTextAnnotation {
1154 fn from(value: crate::plots::figure::TextAnnotation) -> Self {
1155 Self {
1156 position: value.position.to_array(),
1157 text: value.text,
1158 style: value.style.into(),
1159 }
1160 }
1161}
1162
1163impl From<SerializedTextAnnotation> for crate::plots::figure::TextAnnotation {
1164 fn from(value: SerializedTextAnnotation) -> Self {
1165 Self {
1166 position: glam::Vec3::from_array(value.position),
1167 text: value.text,
1168 style: value.style.into(),
1169 }
1170 }
1171}
1172
1173impl From<&MeshRegion> for SerializedMeshRegion {
1174 fn from(value: &MeshRegion) -> Self {
1175 Self {
1176 region_id: value.region_id.clone(),
1177 label: value.label.clone(),
1178 tag: value.tag.clone(),
1179 triangle_ranges: value
1180 .triangle_ranges
1181 .iter()
1182 .copied()
1183 .map(Into::into)
1184 .collect(),
1185 }
1186 }
1187}
1188
1189impl From<&crate::geometry_scene::GeometrySceneRegion> for SerializedMeshRegion {
1190 fn from(value: &crate::geometry_scene::GeometrySceneRegion) -> Self {
1191 Self {
1192 region_id: value.region_id.clone(),
1193 label: value.label.clone(),
1194 tag: value.tag.clone(),
1195 triangle_ranges: value
1196 .triangle_ranges
1197 .iter()
1198 .copied()
1199 .map(Into::into)
1200 .collect(),
1201 }
1202 }
1203}
1204
1205impl From<SerializedMeshRegion> for MeshRegion {
1206 fn from(value: SerializedMeshRegion) -> Self {
1207 MeshRegion {
1208 region_id: value.region_id,
1209 label: value.label,
1210 tag: value.tag,
1211 triangle_ranges: value.triangle_ranges.into_iter().map(Into::into).collect(),
1212 }
1213 }
1214}
1215
1216impl From<SerializedMeshRegion> for crate::geometry_scene::GeometrySceneRegion {
1217 fn from(value: SerializedMeshRegion) -> Self {
1218 crate::geometry_scene::GeometrySceneRegion::new(
1219 value.region_id,
1220 value.label,
1221 value.tag,
1222 value.triangle_ranges.into_iter().map(Into::into).collect(),
1223 )
1224 }
1225}
1226
1227impl From<MeshTriangleRange> for SerializedMeshTriangleRange {
1228 fn from(value: MeshTriangleRange) -> Self {
1229 Self {
1230 start: value.start,
1231 count: value.count,
1232 }
1233 }
1234}
1235
1236impl From<crate::geometry_scene::GeometrySceneTriangleRange> for SerializedMeshTriangleRange {
1237 fn from(value: crate::geometry_scene::GeometrySceneTriangleRange) -> Self {
1238 Self {
1239 start: value.start,
1240 count: value.count,
1241 }
1242 }
1243}
1244
1245impl From<SerializedMeshTriangleRange> for MeshTriangleRange {
1246 fn from(value: SerializedMeshTriangleRange) -> Self {
1247 Self::new(value.start, value.count)
1248 }
1249}
1250
1251impl From<SerializedMeshTriangleRange> for crate::geometry_scene::GeometrySceneTriangleRange {
1252 fn from(value: SerializedMeshTriangleRange) -> Self {
1253 Self::new(value.start, value.count)
1254 }
1255}
1256
1257impl From<&MeshScalarField> for SerializedMeshScalarField {
1258 fn from(value: &MeshScalarField) -> Self {
1259 Self {
1260 field_id: value.field_id.clone(),
1261 label: value.label.clone(),
1262 location: value.location.as_str().to_string(),
1263 values: value.values.clone(),
1264 color_limits: value.color_limits,
1265 colormap: value.colormap.clone(),
1266 alpha: value.alpha,
1267 }
1268 }
1269}
1270
1271impl TryFrom<SerializedMeshScalarField> for MeshScalarField {
1272 type Error = String;
1273
1274 fn try_from(value: SerializedMeshScalarField) -> Result<Self, Self::Error> {
1275 Ok(Self {
1276 field_id: value.field_id,
1277 label: value.label,
1278 location: MeshFieldLocation::parse(&value.location).ok_or_else(|| {
1279 format!("unknown mesh scalar field location '{}'", value.location)
1280 })?,
1281 values: value.values,
1282 color_limits: value.color_limits,
1283 colormap: value.colormap,
1284 alpha: value.alpha,
1285 })
1286 }
1287}
1288
1289impl From<&MeshVectorField> for SerializedMeshVectorField {
1290 fn from(value: &MeshVectorField) -> Self {
1291 Self {
1292 field_id: value.field_id.clone(),
1293 label: value.label.clone(),
1294 location: value.location.as_str().to_string(),
1295 vectors: value
1296 .vectors
1297 .iter()
1298 .map(|vector| vector.to_array())
1299 .collect(),
1300 scale: value.scale,
1301 stride: value.stride,
1302 color_rgba: vec4_to_rgba(value.color),
1303 }
1304 }
1305}
1306
1307impl TryFrom<SerializedMeshVectorField> for MeshVectorField {
1308 type Error = String;
1309
1310 fn try_from(value: SerializedMeshVectorField) -> Result<Self, Self::Error> {
1311 Ok(Self {
1312 field_id: value.field_id,
1313 label: value.label,
1314 location: MeshFieldLocation::parse(&value.location).ok_or_else(|| {
1315 format!("unknown mesh vector field location '{}'", value.location)
1316 })?,
1317 vectors: value.vectors.into_iter().map(Vec3::from_array).collect(),
1318 scale: value.scale,
1319 stride: value.stride,
1320 color: rgba_to_vec4(value.color_rgba),
1321 })
1322 }
1323}
1324
1325impl From<&MeshDeformation> for SerializedMeshDeformation {
1326 fn from(value: &MeshDeformation) -> Self {
1327 Self {
1328 field_id: value.field_id.clone(),
1329 label: value.label.clone(),
1330 displacements: value
1331 .displacements
1332 .iter()
1333 .map(|displacement| displacement.to_array())
1334 .collect(),
1335 scale: value.scale,
1336 }
1337 }
1338}
1339
1340impl From<SerializedMeshDeformation> for MeshDeformation {
1341 fn from(value: SerializedMeshDeformation) -> Self {
1342 Self {
1343 field_id: value.field_id,
1344 label: value.label,
1345 displacements: value
1346 .displacements
1347 .into_iter()
1348 .map(Vec3::from_array)
1349 .collect(),
1350 scale: value.scale,
1351 }
1352 }
1353}
1354
1355#[derive(Debug, Clone, Serialize, Deserialize)]
1357#[serde(rename_all = "camelCase")]
1358pub struct PlotDescriptor {
1359 pub kind: PlotKind,
1360 #[serde(skip_serializing_if = "Option::is_none")]
1361 pub label: Option<String>,
1362 pub axes_index: u32,
1363 pub color_rgba: [f32; 4],
1364 pub visible: bool,
1365}
1366
1367impl PlotDescriptor {
1368 fn from_plot(plot: &PlotElement, axes_index: u32) -> Self {
1369 Self {
1370 kind: PlotKind::from(plot.plot_type()),
1371 label: plot.label(),
1372 axes_index,
1373 color_rgba: vec4_to_rgba(plot.color()),
1374 visible: plot.is_visible(),
1375 }
1376 }
1377}
1378
1379impl ScenePlot {
1380 fn from_plot(plot: &PlotElement, axes_index: u32) -> Self {
1381 match plot {
1382 PlotElement::Line(line) => Self::Line {
1383 x: line.x_data.clone(),
1384 y: line.y_data.clone(),
1385 color_rgba: vec4_to_rgba(line.color),
1386 line_width: line.line_width,
1387 line_style: format!("{:?}", line.line_style),
1388 axes_index,
1389 label: line.label.clone(),
1390 visible: line.visible,
1391 },
1392 PlotElement::ReferenceLine(line) => Self::ReferenceLine {
1393 orientation: match line.orientation {
1394 ReferenceLineOrientation::Vertical => "vertical",
1395 ReferenceLineOrientation::Horizontal => "horizontal",
1396 }
1397 .into(),
1398 value: line.value,
1399 color_rgba: vec4_to_rgba(line.color),
1400 line_width: line.line_width,
1401 line_style: format!("{:?}", line.line_style),
1402 label: line.label.clone(),
1403 display_name: line.display_name.clone(),
1404 label_orientation: line.label_orientation.clone(),
1405 axes_index,
1406 visible: line.visible,
1407 },
1408 PlotElement::Scatter(scatter) => Self::Scatter {
1409 x: scatter.x_data.clone(),
1410 y: scatter.y_data.clone(),
1411 color_rgba: vec4_to_rgba(scatter.color),
1412 marker_size: scatter.marker_size,
1413 marker_style: format!("{:?}", scatter.marker_style),
1414 axes_index,
1415 label: scatter.label.clone(),
1416 visible: scatter.visible,
1417 },
1418 PlotElement::Bar(bar) => Self::Bar {
1419 labels: bar.labels.clone(),
1420 values: bar.values().unwrap_or(&[]).to_vec(),
1421 histogram_bin_edges: bar.histogram_bin_edges().map(|edges| edges.to_vec()),
1422 color_rgba: vec4_to_rgba(bar.color),
1423 outline_color_rgba: bar.outline_color.map(vec4_to_rgba),
1424 bar_width: bar.bar_width,
1425 outline_width: bar.outline_width,
1426 orientation: format!("{:?}", bar.orientation),
1427 group_index: bar.group_index as u32,
1428 group_count: bar.group_count as u32,
1429 stack_offsets: bar.stack_offsets().map(|offsets| offsets.to_vec()),
1430 axes_index,
1431 label: bar.label.clone(),
1432 visible: bar.visible,
1433 },
1434 PlotElement::ErrorBar(error) => Self::ErrorBar {
1435 x: error.x.clone(),
1436 y: error.y.clone(),
1437 err_low: error.y_neg.clone(),
1438 err_high: error.y_pos.clone(),
1439 x_err_low: error.x_neg.clone(),
1440 x_err_high: error.x_pos.clone(),
1441 orientation: format!("{:?}", error.orientation),
1442 color_rgba: vec4_to_rgba(error.color),
1443 line_width: error.line_width,
1444 line_style: format!("{:?}", error.line_style),
1445 cap_width: error.cap_size,
1446 marker_style: error.marker.as_ref().map(|m| format!("{:?}", m.kind)),
1447 marker_size: error.marker.as_ref().map(|m| m.size),
1448 marker_face_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.face_color)),
1449 marker_edge_color: error.marker.as_ref().map(|m| vec4_to_rgba(m.edge_color)),
1450 marker_filled: error.marker.as_ref().map(|m| m.filled),
1451 axes_index,
1452 label: error.label.clone(),
1453 visible: error.visible,
1454 },
1455 PlotElement::Stairs(stairs) => Self::Stairs {
1456 x: stairs.x.clone(),
1457 y: stairs.y.clone(),
1458 color_rgba: vec4_to_rgba(stairs.color),
1459 line_width: stairs.line_width,
1460 axes_index,
1461 label: stairs.label.clone(),
1462 visible: stairs.visible,
1463 },
1464 PlotElement::Stem(stem) => Self::Stem {
1465 x: stem.x.clone(),
1466 y: stem.y.clone(),
1467 baseline: stem.baseline,
1468 color_rgba: vec4_to_rgba(stem.color),
1469 line_width: stem.line_width,
1470 line_style: format!("{:?}", stem.line_style),
1471 baseline_color_rgba: vec4_to_rgba(stem.baseline_color),
1472 baseline_visible: stem.baseline_visible,
1473 marker_color_rgba: vec4_to_rgba(
1474 stem.marker
1475 .as_ref()
1476 .map(|m| m.face_color)
1477 .unwrap_or(stem.color),
1478 ),
1479 marker_size: stem.marker.as_ref().map(|m| m.size).unwrap_or(0.0),
1480 marker_filled: stem.marker.as_ref().map(|m| m.filled).unwrap_or(false),
1481 axes_index,
1482 label: stem.label.clone(),
1483 visible: stem.visible,
1484 },
1485 PlotElement::Area(area) => Self::Area {
1486 x: area.x.clone(),
1487 y: area.y.clone(),
1488 lower_y: area.lower_y.clone(),
1489 baseline: area.baseline,
1490 color_rgba: vec4_to_rgba(area.color),
1491 axes_index,
1492 label: area.label.clone(),
1493 visible: area.visible,
1494 },
1495 PlotElement::Quiver(quiver) => Self::Quiver {
1496 x: quiver.x.clone(),
1497 y: quiver.y.clone(),
1498 u: quiver.u.clone(),
1499 v: quiver.v.clone(),
1500 color_rgba: vec4_to_rgba(quiver.color),
1501 line_width: quiver.line_width,
1502 scale: quiver.scale,
1503 head_size: quiver.head_size,
1504 axes_index,
1505 label: quiver.label.clone(),
1506 visible: quiver.visible,
1507 },
1508 PlotElement::Surface(surface) => Self::Surface {
1509 x: surface.x_data.clone(),
1510 y: surface.y_data.clone(),
1511 z: surface.z_data.clone().unwrap_or_default(),
1512 colormap: format!("{:?}", surface.colormap),
1513 shading_mode: format!("{:?}", surface.shading_mode),
1514 wireframe: surface.wireframe,
1515 alpha: surface.alpha,
1516 flatten_z: surface.flatten_z,
1517 image_mode: surface.image_mode,
1518 color_grid_rgba: surface.color_grid.as_ref().map(|grid| {
1519 grid.iter()
1520 .map(|row| row.iter().map(|color| vec4_to_rgba(*color)).collect())
1521 .collect()
1522 }),
1523 color_limits: surface.color_limits.map(|(lo, hi)| [lo, hi]),
1524 axes_index,
1525 label: surface.label.clone(),
1526 visible: surface.visible,
1527 },
1528 PlotElement::Patch(patch) => Self::Patch {
1529 vertices: patch
1530 .vertices()
1531 .iter()
1532 .map(|point| vec3_to_xyz(*point))
1533 .collect(),
1534 faces: patch
1535 .faces()
1536 .iter()
1537 .map(|face| face.iter().map(|idx| *idx as u32).collect())
1538 .collect(),
1539 face_color_rgba: vec4_to_rgba(patch.face_color()),
1540 edge_color_rgba: vec4_to_rgba(patch.edge_color()),
1541 face_color_mode: format!("{:?}", patch.face_color_mode()),
1542 edge_color_mode: format!("{:?}", patch.edge_color_mode()),
1543 face_alpha: patch.face_alpha(),
1544 edge_alpha: patch.edge_alpha(),
1545 line_width: patch.line_width(),
1546 axes_index,
1547 label: patch.label().map(str::to_string),
1548 visible: patch.is_visible(),
1549 force_3d: patch.force_3d(),
1550 },
1551 PlotElement::Mesh(mesh) => Self::Mesh {
1552 vertices: mesh
1553 .vertices()
1554 .iter()
1555 .map(|point| vec3_to_xyz(*point))
1556 .collect(),
1557 triangles: mesh.triangles().to_vec(),
1558 mesh_id: mesh.mesh_id().map(str::to_string),
1559 face_color_rgba: vec4_to_rgba(mesh.face_color()),
1560 edge_color_rgba: vec4_to_rgba(mesh.edge_color()),
1561 face_alpha: mesh.face_alpha(),
1562 edge_alpha: mesh.edge_alpha(),
1563 edge_width: mesh.edge_width(),
1564 edge_mode: mesh.edge_mode().as_str().to_string(),
1565 feature_edge_groups: mesh
1566 .feature_edge_groups()
1567 .map(|groups| groups.to_vec())
1568 .unwrap_or_default(),
1569 vertex_colors_rgba: mesh
1570 .vertex_colors()
1571 .map(|colors| colors.iter().copied().map(vec4_to_rgba).collect())
1572 .unwrap_or_default(),
1573 triangle_colors_rgba: mesh
1574 .triangle_colors()
1575 .map(|colors| colors.iter().copied().map(vec4_to_rgba).collect())
1576 .unwrap_or_default(),
1577 axes_index,
1578 label: mesh.label().map(str::to_string),
1579 regions: mesh.regions().iter().map(Into::into).collect(),
1580 highlighted_region_id: mesh.highlighted_region_id().map(str::to_string),
1581 highlight_color_rgba: Some(vec4_to_rgba(mesh.highlight_color())),
1582 scalar_field: mesh.scalar_field().map(|field| Box::new(field.into())),
1583 vector_field: mesh.vector_field().map(|field| Box::new(field.into())),
1584 deformation: mesh.deformation().map(|field| Box::new(field.into())),
1585 visible: mesh.is_visible(),
1586 },
1587 PlotElement::Line3(line) => Self::Line3 {
1588 x: line.x_data.clone(),
1589 y: line.y_data.clone(),
1590 z: line.z_data.clone(),
1591 color_rgba: vec4_to_rgba(line.color),
1592 line_width: line.line_width,
1593 line_style: format!("{:?}", line.line_style),
1594 axes_index,
1595 label: line.label.clone(),
1596 visible: line.visible,
1597 },
1598 PlotElement::Scatter3(scatter3) => Self::Scatter3 {
1599 points: scatter3
1600 .points
1601 .iter()
1602 .map(|point| vec3_to_xyz(*point))
1603 .collect(),
1604 colors_rgba: scatter3
1605 .colors
1606 .iter()
1607 .map(|color| vec4_to_rgba(*color))
1608 .collect(),
1609 point_size: scatter3.point_size,
1610 point_sizes: scatter3.point_sizes.clone(),
1611 axes_index,
1612 label: scatter3.label.clone(),
1613 visible: scatter3.visible,
1614 },
1615 PlotElement::Contour(contour) => Self::Contour {
1616 vertices: contour
1617 .cpu_vertices()
1618 .unwrap_or(&[])
1619 .iter()
1620 .cloned()
1621 .map(Into::into)
1622 .collect(),
1623 bounds_min: vec3_to_xyz(contour.bounds().min),
1624 bounds_max: vec3_to_xyz(contour.bounds().max),
1625 base_z: contour.base_z,
1626 line_width: contour.line_width,
1627 axes_index,
1628 label: contour.label.clone(),
1629 visible: contour.visible,
1630 force_3d: contour.force_3d,
1631 },
1632 PlotElement::ContourFill(fill) => Self::ContourFill {
1633 vertices: fill
1634 .cpu_vertices()
1635 .unwrap_or(&[])
1636 .iter()
1637 .cloned()
1638 .map(Into::into)
1639 .collect(),
1640 bounds_min: vec3_to_xyz(fill.bounds().min),
1641 bounds_max: vec3_to_xyz(fill.bounds().max),
1642 axes_index,
1643 label: fill.label.clone(),
1644 visible: fill.visible,
1645 },
1646 PlotElement::Pie(pie) => Self::Pie {
1647 values: pie.values.clone(),
1648 colors_rgba: pie.colors.iter().map(|c| vec4_to_rgba(*c)).collect(),
1649 slice_labels: pie.slice_labels.clone(),
1650 label_format: pie.label_format.clone(),
1651 explode: pie.explode.clone(),
1652 axes_index,
1653 label: pie.label.clone(),
1654 visible: pie.visible,
1655 },
1656 }
1657 }
1658
1659 fn apply_to_figure(self, figure: &mut Figure) -> Result<(), String> {
1660 match self {
1661 ScenePlot::Line {
1662 x,
1663 y,
1664 color_rgba,
1665 line_width,
1666 line_style,
1667 axes_index,
1668 label,
1669 visible,
1670 } => {
1671 let mut line = LinePlot::new(x, y)?;
1672 line.set_color(rgba_to_vec4(color_rgba));
1673 line.set_line_width(line_width);
1674 line.set_line_style(parse_line_style(&line_style));
1675 line.label = label;
1676 line.set_visible(visible);
1677 figure.add_line_plot_on_axes(line, axes_index as usize);
1678 }
1679 ScenePlot::ReferenceLine {
1680 orientation,
1681 value,
1682 color_rgba,
1683 line_width,
1684 line_style,
1685 label,
1686 display_name,
1687 label_orientation,
1688 axes_index,
1689 visible,
1690 } => {
1691 let orientation = parse_reference_line_orientation(&orientation)?;
1692 let mut line = ReferenceLine::new(orientation, value)?.with_style(
1693 rgba_to_vec4(color_rgba),
1694 line_width,
1695 parse_line_style(&line_style),
1696 );
1697 line.label = label;
1698 line.display_name = display_name;
1699 line.label_orientation = label_orientation;
1700 line.visible = visible;
1701 figure.add_reference_line_on_axes(line, axes_index as usize);
1702 }
1703 ScenePlot::Scatter {
1704 x,
1705 y,
1706 color_rgba,
1707 marker_size,
1708 marker_style,
1709 axes_index,
1710 label,
1711 visible,
1712 } => {
1713 let mut scatter = ScatterPlot::new(x, y)?;
1714 scatter.set_color(rgba_to_vec4(color_rgba));
1715 scatter.set_marker_size(marker_size);
1716 scatter.set_marker_style(parse_marker_style(&marker_style));
1717 scatter.label = label;
1718 scatter.set_visible(visible);
1719 figure.add_scatter_plot_on_axes(scatter, axes_index as usize);
1720 }
1721 ScenePlot::Bar {
1722 labels,
1723 values,
1724 histogram_bin_edges,
1725 color_rgba,
1726 outline_color_rgba,
1727 bar_width,
1728 outline_width,
1729 orientation,
1730 group_index,
1731 group_count,
1732 stack_offsets,
1733 axes_index,
1734 label,
1735 visible,
1736 } => {
1737 let mut bar = BarChart::new(labels, values)?
1738 .with_style(rgba_to_vec4(color_rgba), bar_width)
1739 .with_orientation(parse_bar_orientation(&orientation))
1740 .with_group(group_index as usize, group_count as usize);
1741 if let Some(edges) = histogram_bin_edges {
1742 bar.set_histogram_bin_edges(edges);
1743 }
1744 if let Some(offsets) = stack_offsets {
1745 bar = bar.with_stack_offsets(offsets);
1746 }
1747 if let Some(outline) = outline_color_rgba {
1748 bar = bar.with_outline(rgba_to_vec4(outline), outline_width);
1749 }
1750 bar.label = label;
1751 bar.set_visible(visible);
1752 figure.add_bar_chart_on_axes(bar, axes_index as usize);
1753 }
1754 ScenePlot::ErrorBar {
1755 x,
1756 y,
1757 err_low,
1758 err_high,
1759 x_err_low,
1760 x_err_high,
1761 orientation,
1762 color_rgba,
1763 line_width,
1764 line_style,
1765 cap_width,
1766 marker_style,
1767 marker_size,
1768 marker_face_color,
1769 marker_edge_color,
1770 marker_filled,
1771 axes_index,
1772 label,
1773 visible,
1774 } => {
1775 let mut error = if orientation.eq_ignore_ascii_case("Both") {
1776 ErrorBar::new_both(x, y, x_err_low, x_err_high, err_low, err_high)?
1777 } else {
1778 ErrorBar::new_vertical(x, y, err_low, err_high)?
1779 }
1780 .with_style(
1781 rgba_to_vec4(color_rgba),
1782 line_width,
1783 parse_line_style_name(&line_style),
1784 cap_width,
1785 );
1786 if let Some(size) = marker_size {
1787 error.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1788 kind: parse_marker_style(marker_style.as_deref().unwrap_or("Circle")),
1789 size,
1790 edge_color: marker_edge_color
1791 .map(rgba_to_vec4)
1792 .unwrap_or(rgba_to_vec4(color_rgba)),
1793 face_color: marker_face_color
1794 .map(rgba_to_vec4)
1795 .unwrap_or(rgba_to_vec4(color_rgba)),
1796 filled: marker_filled.unwrap_or(false),
1797 }));
1798 }
1799 error.label = label;
1800 error.set_visible(visible);
1801 figure.add_errorbar_on_axes(error, axes_index as usize);
1802 }
1803 ScenePlot::Stairs {
1804 x,
1805 y,
1806 color_rgba,
1807 line_width,
1808 axes_index,
1809 label,
1810 visible,
1811 } => {
1812 let mut stairs = StairsPlot::new(x, y)?;
1813 stairs.color = rgba_to_vec4(color_rgba);
1814 stairs.line_width = line_width;
1815 stairs.label = label;
1816 stairs.set_visible(visible);
1817 figure.add_stairs_plot_on_axes(stairs, axes_index as usize);
1818 }
1819 ScenePlot::Stem {
1820 x,
1821 y,
1822 baseline,
1823 color_rgba,
1824 line_width,
1825 line_style,
1826 baseline_color_rgba,
1827 baseline_visible,
1828 marker_color_rgba,
1829 marker_size,
1830 marker_filled,
1831 axes_index,
1832 label,
1833 visible,
1834 } => {
1835 let mut stem = StemPlot::new(x, y)?;
1836 stem = stem
1837 .with_style(
1838 rgba_to_vec4(color_rgba),
1839 line_width,
1840 parse_line_style_name(&line_style),
1841 baseline,
1842 )
1843 .with_baseline_style(rgba_to_vec4(baseline_color_rgba), baseline_visible);
1844 if marker_size > 0.0 {
1845 stem.set_marker(Some(crate::plots::line::LineMarkerAppearance {
1846 kind: crate::plots::scatter::MarkerStyle::Circle,
1847 size: marker_size,
1848 edge_color: rgba_to_vec4(marker_color_rgba),
1849 face_color: rgba_to_vec4(marker_color_rgba),
1850 filled: marker_filled,
1851 }));
1852 }
1853 stem.label = label;
1854 stem.set_visible(visible);
1855 figure.add_stem_plot_on_axes(stem, axes_index as usize);
1856 }
1857 ScenePlot::Area {
1858 x,
1859 y,
1860 lower_y,
1861 baseline,
1862 color_rgba,
1863 axes_index,
1864 label,
1865 visible,
1866 } => {
1867 let mut area = AreaPlot::new(x, y)?;
1868 if let Some(lower_y) = lower_y {
1869 area = area.with_lower_curve(lower_y);
1870 }
1871 area.baseline = baseline;
1872 area.color = rgba_to_vec4(color_rgba);
1873 area.label = label;
1874 area.set_visible(visible);
1875 figure.add_area_plot_on_axes(area, axes_index as usize);
1876 }
1877 ScenePlot::Quiver {
1878 x,
1879 y,
1880 u,
1881 v,
1882 color_rgba,
1883 line_width,
1884 scale,
1885 head_size,
1886 axes_index,
1887 label,
1888 visible,
1889 } => {
1890 let mut quiver = QuiverPlot::new(x, y, u, v)?
1891 .with_style(rgba_to_vec4(color_rgba), line_width, scale, head_size)
1892 .with_label(label.unwrap_or_else(|| "Data".to_string()));
1893 quiver.set_visible(visible);
1894 figure.add_quiver_plot_on_axes(quiver, axes_index as usize);
1895 }
1896 ScenePlot::Surface {
1897 x,
1898 y,
1899 z,
1900 colormap,
1901 shading_mode,
1902 wireframe,
1903 alpha,
1904 flatten_z,
1905 image_mode,
1906 color_grid_rgba,
1907 color_limits,
1908 axes_index,
1909 label,
1910 visible,
1911 } => {
1912 let mut surface = SurfacePlot::new(x, y, z)?;
1913 surface.colormap = parse_colormap(&colormap);
1914 surface.shading_mode = parse_shading_mode(&shading_mode);
1915 surface.wireframe = wireframe;
1916 surface.alpha = alpha.clamp(0.0, 1.0);
1917 surface.flatten_z = flatten_z;
1918 surface.image_mode = image_mode;
1919 surface.color_grid = color_grid_rgba.map(|grid| {
1920 grid.into_iter()
1921 .map(|row| row.into_iter().map(rgba_to_vec4).collect())
1922 .collect()
1923 });
1924 surface.color_limits = color_limits.map(|[lo, hi]| (lo, hi));
1925 surface.label = label;
1926 surface.visible = visible;
1927 figure.add_surface_plot_on_axes(surface, axes_index as usize);
1928 }
1929 ScenePlot::Patch {
1930 vertices,
1931 faces,
1932 face_color_rgba,
1933 edge_color_rgba,
1934 face_color_mode,
1935 edge_color_mode,
1936 face_alpha,
1937 edge_alpha,
1938 line_width,
1939 axes_index,
1940 label,
1941 visible,
1942 force_3d,
1943 } => {
1944 let vertices: Vec<Vec3> = vertices.into_iter().map(xyz_to_vec3).collect();
1945 let faces: Vec<Vec<usize>> = faces
1946 .into_iter()
1947 .map(|face| face.into_iter().map(|idx| idx as usize).collect())
1948 .collect();
1949 let mut patch = PatchPlot::new(vertices, faces)?;
1950 patch.set_face_color(rgba_to_vec4(face_color_rgba));
1951 patch.set_edge_color(rgba_to_vec4(edge_color_rgba));
1952 patch.set_face_color_mode(parse_patch_face_color_mode(&face_color_mode));
1953 patch.set_edge_color_mode(parse_patch_edge_color_mode(&edge_color_mode));
1954 patch.set_face_alpha(face_alpha);
1955 patch.set_edge_alpha(edge_alpha);
1956 patch.set_line_width(line_width);
1957 patch.set_label(label);
1958 patch.set_visible(visible);
1959 patch.set_force_3d(force_3d);
1960 figure.add_patch_plot_on_axes(patch, axes_index as usize);
1961 }
1962 ScenePlot::Mesh {
1963 vertices,
1964 triangles,
1965 mesh_id,
1966 face_color_rgba,
1967 edge_color_rgba,
1968 face_alpha,
1969 edge_alpha,
1970 edge_width,
1971 edge_mode,
1972 feature_edge_groups,
1973 vertex_colors_rgba,
1974 triangle_colors_rgba,
1975 axes_index,
1976 label,
1977 regions,
1978 highlighted_region_id,
1979 highlight_color_rgba,
1980 scalar_field,
1981 vector_field,
1982 deformation,
1983 visible,
1984 } => {
1985 let vertices: Vec<Vec3> = vertices.into_iter().map(xyz_to_vec3).collect();
1986 let mut mesh = MeshPlot::new(vertices, triangles)?;
1987 mesh.set_mesh_id(mesh_id);
1988 mesh.set_face_color(rgba_to_vec4(face_color_rgba));
1989 mesh.set_edge_color(rgba_to_vec4(edge_color_rgba));
1990 mesh.set_face_alpha(face_alpha);
1991 mesh.set_edge_alpha(edge_alpha);
1992 mesh.set_edge_width(edge_width);
1993 mesh.set_edge_mode(parse_mesh_edge_mode(&edge_mode));
1994 if !feature_edge_groups.is_empty() {
1995 mesh.set_feature_edge_groups(Some(feature_edge_groups))?;
1996 }
1997 if !vertex_colors_rgba.is_empty() {
1998 mesh.set_vertex_colors(Some(
1999 vertex_colors_rgba.into_iter().map(rgba_to_vec4).collect(),
2000 ))?;
2001 }
2002 if !triangle_colors_rgba.is_empty() {
2003 mesh.set_triangle_colors(Some(
2004 triangle_colors_rgba.into_iter().map(rgba_to_vec4).collect(),
2005 ))?;
2006 }
2007 mesh.set_label(label);
2008 mesh.set_regions(regions.into_iter().map(Into::into).collect());
2009 mesh.set_highlighted_region_id(highlighted_region_id);
2010 if let Some(color) = highlight_color_rgba {
2011 mesh.set_highlight_color(rgba_to_vec4(color));
2012 }
2013 if let Some(field) = scalar_field {
2014 mesh.set_scalar_field(Some((*field).try_into()?))?;
2015 }
2016 if let Some(field) = vector_field {
2017 mesh.set_vector_field(Some((*field).try_into()?))?;
2018 }
2019 if let Some(field) = deformation {
2020 mesh.set_deformation(Some((*field).into()))?;
2021 }
2022 mesh.set_visible(visible);
2023 figure.add_mesh_plot_on_axes(mesh, axes_index as usize);
2024 }
2025 ScenePlot::Line3 {
2026 x,
2027 y,
2028 z,
2029 color_rgba,
2030 line_width,
2031 line_style,
2032 axes_index,
2033 label,
2034 visible,
2035 } => {
2036 let mut plot = Line3Plot::new(x, y, z)?
2037 .with_style(
2038 rgba_to_vec4(color_rgba),
2039 line_width,
2040 parse_line_style_name(&line_style),
2041 )
2042 .with_label(label.unwrap_or_else(|| "Data".to_string()));
2043 plot.set_visible(visible);
2044 figure.add_line3_plot_on_axes(plot, axes_index as usize);
2045 }
2046 ScenePlot::Scatter3 {
2047 points,
2048 colors_rgba,
2049 point_size,
2050 point_sizes,
2051 axes_index,
2052 label,
2053 visible,
2054 } => {
2055 let points: Vec<Vec3> = points.into_iter().map(xyz_to_vec3).collect();
2056 let colors: Vec<Vec4> = colors_rgba.into_iter().map(rgba_to_vec4).collect();
2057 let mut scatter3 = Scatter3Plot::new(points)?;
2058 if !colors.is_empty() {
2059 scatter3 = scatter3.with_colors(colors)?;
2060 }
2061 scatter3.point_size = point_size.max(1.0);
2062 scatter3.point_sizes = point_sizes;
2063 scatter3.label = label;
2064 scatter3.visible = visible;
2065 figure.add_scatter3_plot_on_axes(scatter3, axes_index as usize);
2066 }
2067 ScenePlot::Contour {
2068 vertices,
2069 bounds_min,
2070 bounds_max,
2071 base_z,
2072 line_width,
2073 axes_index,
2074 label,
2075 visible,
2076 force_3d,
2077 } => {
2078 let mut contour = ContourPlot::from_vertices(
2079 vertices.into_iter().map(Into::into).collect(),
2080 base_z,
2081 serialized_bounds(bounds_min, bounds_max),
2082 )
2083 .with_line_width(line_width)
2084 .with_force_3d(force_3d);
2085 contour.label = label;
2086 contour.set_visible(visible);
2087 figure.add_contour_plot_on_axes(contour, axes_index as usize);
2088 }
2089 ScenePlot::ContourFill {
2090 vertices,
2091 bounds_min,
2092 bounds_max,
2093 axes_index,
2094 label,
2095 visible,
2096 } => {
2097 let mut fill = ContourFillPlot::from_vertices(
2098 vertices.into_iter().map(Into::into).collect(),
2099 serialized_bounds(bounds_min, bounds_max),
2100 );
2101 fill.label = label;
2102 fill.set_visible(visible);
2103 figure.add_contour_fill_plot_on_axes(fill, axes_index as usize);
2104 }
2105 ScenePlot::Pie {
2106 values,
2107 colors_rgba,
2108 slice_labels,
2109 label_format,
2110 explode,
2111 axes_index,
2112 label,
2113 visible,
2114 } => {
2115 let mut pie = crate::plots::PieChart::new(
2116 values,
2117 Some(colors_rgba.into_iter().map(rgba_to_vec4).collect()),
2118 )?
2119 .with_slice_labels(slice_labels)
2120 .with_explode(explode);
2121 if let Some(fmt) = label_format {
2122 pie = pie.with_label_format(fmt);
2123 }
2124 pie.label = label;
2125 pie.set_visible(visible);
2126 figure.add_pie_chart_on_axes(pie, axes_index as usize);
2127 }
2128 ScenePlot::Unsupported { .. } => {}
2129 }
2130 Ok(())
2131 }
2132}
2133
2134fn parse_line_style(value: &str) -> crate::plots::LineStyle {
2135 match value {
2136 "Dashed" => crate::plots::LineStyle::Dashed,
2137 "Dotted" => crate::plots::LineStyle::Dotted,
2138 "DashDot" => crate::plots::LineStyle::DashDot,
2139 _ => crate::plots::LineStyle::Solid,
2140 }
2141}
2142
2143fn parse_bar_orientation(value: &str) -> crate::plots::bar::Orientation {
2144 match value {
2145 "Horizontal" => crate::plots::bar::Orientation::Horizontal,
2146 _ => crate::plots::bar::Orientation::Vertical,
2147 }
2148}
2149
2150fn parse_reference_line_orientation(value: &str) -> Result<ReferenceLineOrientation, String> {
2151 match value.to_ascii_lowercase().as_str() {
2152 "horizontal" => Ok(ReferenceLineOrientation::Horizontal),
2153 "vertical" => Ok(ReferenceLineOrientation::Vertical),
2154 _ => Err(format!(
2155 "unknown reference line orientation '{value}'; expected 'horizontal' or 'vertical'"
2156 )),
2157 }
2158}
2159
2160fn parse_marker_style(value: &str) -> MarkerStyle {
2161 match value {
2162 "Square" => MarkerStyle::Square,
2163 "Triangle" => MarkerStyle::Triangle,
2164 "Diamond" => MarkerStyle::Diamond,
2165 "Plus" => MarkerStyle::Plus,
2166 "Cross" => MarkerStyle::Cross,
2167 "Star" => MarkerStyle::Star,
2168 "Hexagon" => MarkerStyle::Hexagon,
2169 _ => MarkerStyle::Circle,
2170 }
2171}
2172
2173fn parse_colormap(value: &str) -> ColorMap {
2174 ColorMap::from_name(value).unwrap_or(ColorMap::Parula)
2175}
2176
2177fn parse_shading_mode(value: &str) -> ShadingMode {
2178 match value {
2179 "Flat" => ShadingMode::Flat,
2180 "Smooth" => ShadingMode::Smooth,
2181 "Faceted" => ShadingMode::Faceted,
2182 "None" => ShadingMode::None,
2183 _ => ShadingMode::Smooth,
2184 }
2185}
2186
2187fn parse_patch_face_color_mode(value: &str) -> PatchFaceColorMode {
2188 match value {
2189 "None" => PatchFaceColorMode::None,
2190 "Flat" => PatchFaceColorMode::Flat,
2191 _ => PatchFaceColorMode::Color,
2192 }
2193}
2194
2195fn parse_patch_edge_color_mode(value: &str) -> PatchEdgeColorMode {
2196 match value {
2197 "None" => PatchEdgeColorMode::None,
2198 _ => PatchEdgeColorMode::Color,
2199 }
2200}
2201
2202fn parse_mesh_edge_mode(value: &str) -> MeshEdgeMode {
2203 MeshEdgeMode::parse(value).unwrap_or_default()
2204}
2205
2206fn xyz_to_vec3(value: [f32; 3]) -> Vec3 {
2207 Vec3::new(value[0], value[1], value[2])
2208}
2209
2210fn serialized_bounds(min: [f32; 3], max: [f32; 3]) -> BoundingBox {
2211 BoundingBox::new(xyz_to_vec3(min), xyz_to_vec3(max))
2212}
2213
2214fn vec3_to_xyz(value: Vec3) -> [f32; 3] {
2215 [value.x, value.y, value.z]
2216}
2217
2218fn rgba_to_vec4(value: [f32; 4]) -> Vec4 {
2219 Vec4::new(value[0], value[1], value[2], value[3])
2220}
2221
2222#[derive(Debug, Clone, Serialize, Deserialize)]
2223#[serde(rename_all = "camelCase")]
2224pub struct SerializedVertex {
2225 position: [f32; 3],
2226 color_rgba: [f32; 4],
2227 normal: [f32; 3],
2228 tex_coords: [f32; 2],
2229}
2230
2231impl From<Vertex> for SerializedVertex {
2232 fn from(value: Vertex) -> Self {
2233 Self {
2234 position: value.position,
2235 color_rgba: value.color,
2236 normal: value.normal,
2237 tex_coords: value.tex_coords,
2238 }
2239 }
2240}
2241
2242impl From<SerializedVertex> for Vertex {
2243 fn from(value: SerializedVertex) -> Self {
2244 Self {
2245 position: value.position,
2246 color: value.color_rgba,
2247 normal: value.normal,
2248 tex_coords: value.tex_coords,
2249 }
2250 }
2251}
2252
2253#[derive(Debug, Clone, Serialize, Deserialize)]
2255#[serde(rename_all = "camelCase")]
2256pub struct FigureLegendEntry {
2257 pub label: String,
2258 pub plot_type: PlotKind,
2259 pub color_rgba: [f32; 4],
2260}
2261
2262impl From<LegendEntry> for FigureLegendEntry {
2263 fn from(entry: LegendEntry) -> Self {
2264 Self {
2265 label: entry.label,
2266 plot_type: PlotKind::from(entry.plot_type),
2267 color_rgba: vec4_to_rgba(entry.color),
2268 }
2269 }
2270}
2271
2272#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2274#[serde(rename_all = "snake_case")]
2275pub enum PlotKind {
2276 Line,
2277 Line3,
2278 Scatter,
2279 Bar,
2280 ErrorBar,
2281 Stairs,
2282 Stem,
2283 Area,
2284 Quiver,
2285 Pie,
2286 Image,
2287 Surface,
2288 Mesh,
2289 Patch,
2290 Scatter3,
2291 Contour,
2292 ContourFill,
2293 ReferenceLine,
2294}
2295
2296impl From<PlotType> for PlotKind {
2297 fn from(value: PlotType) -> Self {
2298 match value {
2299 PlotType::Line => Self::Line,
2300 PlotType::Line3 => Self::Line3,
2301 PlotType::Scatter => Self::Scatter,
2302 PlotType::Bar => Self::Bar,
2303 PlotType::ErrorBar => Self::ErrorBar,
2304 PlotType::Stairs => Self::Stairs,
2305 PlotType::Stem => Self::Stem,
2306 PlotType::Area => Self::Area,
2307 PlotType::Quiver => Self::Quiver,
2308 PlotType::Pie => Self::Pie,
2309 PlotType::Surface => Self::Surface,
2310 PlotType::Mesh => Self::Mesh,
2311 PlotType::Patch => Self::Patch,
2312 PlotType::Scatter3 => Self::Scatter3,
2313 PlotType::Contour => Self::Contour,
2314 PlotType::ContourFill => Self::ContourFill,
2315 PlotType::ReferenceLine => Self::ReferenceLine,
2316 }
2317 }
2318}
2319
2320fn parse_line_style_name(name: &str) -> crate::plots::line::LineStyle {
2321 match name.to_ascii_lowercase().as_str() {
2322 "dashed" => crate::plots::line::LineStyle::Dashed,
2323 "dotted" => crate::plots::line::LineStyle::Dotted,
2324 "dashdot" => crate::plots::line::LineStyle::DashDot,
2325 _ => crate::plots::line::LineStyle::Solid,
2326 }
2327}
2328
2329fn parse_colormap_name(name: &str) -> crate::plots::surface::ColorMap {
2330 crate::plots::surface::ColorMap::from_name(name)
2331 .unwrap_or(crate::plots::surface::ColorMap::Parula)
2332}
2333
2334fn vec4_to_rgba(value: Vec4) -> [f32; 4] {
2335 [value.x, value.y, value.z, value.w]
2336}
2337
2338fn deserialize_f64_lossy<'de, D>(deserializer: D) -> Result<f64, D::Error>
2339where
2340 D: serde::Deserializer<'de>,
2341{
2342 let value = Option::<f64>::deserialize(deserializer)?;
2343 Ok(value.unwrap_or(f64::NAN))
2344}
2345
2346fn deserialize_vec_f64_lossy<'de, D>(deserializer: D) -> Result<Vec<f64>, D::Error>
2347where
2348 D: serde::Deserializer<'de>,
2349{
2350 let values = Vec::<Option<f64>>::deserialize(deserializer)?;
2351 Ok(values
2352 .into_iter()
2353 .map(|value| value.unwrap_or(f64::NAN))
2354 .collect())
2355}
2356
2357fn deserialize_vec_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<f32>, D::Error>
2358where
2359 D: serde::Deserializer<'de>,
2360{
2361 let values = Vec::<Option<f32>>::deserialize(deserializer)?;
2362 Ok(values
2363 .into_iter()
2364 .map(|value| value.unwrap_or(f32::NAN))
2365 .collect())
2366}
2367
2368fn deserialize_option_vec_f64_lossy<'de, D>(deserializer: D) -> Result<Option<Vec<f64>>, D::Error>
2369where
2370 D: serde::Deserializer<'de>,
2371{
2372 let values = Option::<Vec<Option<f64>>>::deserialize(deserializer)?;
2373 Ok(values.map(|items| {
2374 items
2375 .into_iter()
2376 .map(|value| value.unwrap_or(f64::NAN))
2377 .collect()
2378 }))
2379}
2380
2381fn deserialize_matrix_f64_lossy<'de, D>(deserializer: D) -> Result<Vec<Vec<f64>>, D::Error>
2382where
2383 D: serde::Deserializer<'de>,
2384{
2385 let rows = Vec::<Vec<Option<f64>>>::deserialize(deserializer)?;
2386 Ok(rows
2387 .into_iter()
2388 .map(|row| {
2389 row.into_iter()
2390 .map(|value| value.unwrap_or(f64::NAN))
2391 .collect()
2392 })
2393 .collect())
2394}
2395
2396fn deserialize_option_pair_f64_lossy<'de, D>(deserializer: D) -> Result<Option<[f64; 2]>, D::Error>
2397where
2398 D: serde::Deserializer<'de>,
2399{
2400 let value = Option::<[Option<f64>; 2]>::deserialize(deserializer)?;
2401 Ok(value.map(|pair| [pair[0].unwrap_or(f64::NAN), pair[1].unwrap_or(f64::NAN)]))
2402}
2403
2404fn deserialize_option_vec_f32_lossy<'de, D>(deserializer: D) -> Result<Option<Vec<f32>>, D::Error>
2405where
2406 D: serde::Deserializer<'de>,
2407{
2408 let values = Option::<Vec<Option<f32>>>::deserialize(deserializer)?;
2409 Ok(values.map(|items| {
2410 items
2411 .into_iter()
2412 .map(|value| value.unwrap_or(f32::NAN))
2413 .collect()
2414 }))
2415}
2416
2417fn deserialize_vec_xyz_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<[f32; 3]>, D::Error>
2418where
2419 D: serde::Deserializer<'de>,
2420{
2421 let values = Vec::<[Option<f32>; 3]>::deserialize(deserializer)?;
2422 Ok(values
2423 .into_iter()
2424 .map(|xyz| {
2425 [
2426 xyz[0].unwrap_or(f32::NAN),
2427 xyz[1].unwrap_or(f32::NAN),
2428 xyz[2].unwrap_or(f32::NAN),
2429 ]
2430 })
2431 .collect())
2432}
2433
2434fn deserialize_vec_rgba_f32_lossy<'de, D>(deserializer: D) -> Result<Vec<[f32; 4]>, D::Error>
2435where
2436 D: serde::Deserializer<'de>,
2437{
2438 let values = Vec::<[Option<f32>; 4]>::deserialize(deserializer)?;
2439 Ok(values
2440 .into_iter()
2441 .map(|rgba| {
2442 [
2443 rgba[0].unwrap_or(f32::NAN),
2444 rgba[1].unwrap_or(f32::NAN),
2445 rgba[2].unwrap_or(f32::NAN),
2446 rgba[3].unwrap_or(f32::NAN),
2447 ]
2448 })
2449 .collect())
2450}
2451
2452#[cfg(test)]
2453mod tests {
2454 use super::*;
2455 use crate::plots::{
2456 Figure, Line3Plot, LinePlot, PatchPlot, Scatter3Plot, ScatterPlot, SurfacePlot,
2457 };
2458 use glam::{Vec3, Vec4};
2459
2460 #[test]
2461 fn capture_snapshot_reflects_layout_and_metadata() {
2462 let mut figure = Figure::new()
2463 .with_title("Demo")
2464 .with_sg_title("Overview")
2465 .with_labels("X", "Y")
2466 .with_grid(false)
2467 .with_subplot_grid(1, 2);
2468 figure.set_name("Window Name");
2469 figure.set_number_title(false);
2470 figure.set_visible(false);
2471 figure.set_background_color(Vec4::new(0.0, 0.0, 0.0, 1.0));
2472 let line = LinePlot::new(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
2473 figure.add_line_plot_on_axes(line, 1);
2474
2475 let snapshot = FigureSnapshot::capture(&figure);
2476 assert_eq!(snapshot.layout.axes_rows, 1);
2477 assert_eq!(snapshot.layout.axes_cols, 2);
2478 assert_eq!(snapshot.metadata.title.as_deref(), Some("Demo"));
2479 assert_eq!(snapshot.metadata.name.as_deref(), Some("Window Name"));
2480 assert!(!snapshot.metadata.number_title);
2481 assert!(!snapshot.metadata.visible);
2482 assert_eq!(snapshot.metadata.sg_title.as_deref(), Some("Overview"));
2483 assert_eq!(snapshot.metadata.background_rgba, [0.0, 0.0, 0.0, 1.0]);
2484 assert_eq!(snapshot.metadata.legend_entries.len(), 0);
2485 assert_eq!(snapshot.plots.len(), 1);
2486 assert_eq!(snapshot.plots[0].axes_index, 1);
2487 assert!(!snapshot.metadata.grid_enabled);
2488 }
2489
2490 #[test]
2491 fn sg_title_style_omitted_when_sg_title_absent() {
2492 let figure = Figure::new().with_title("Only regular title");
2493 let snapshot = FigureSnapshot::capture(&figure);
2494 assert!(snapshot.metadata.sg_title.is_none());
2495 assert!(
2496 snapshot.metadata.sg_title_style.is_none(),
2497 "sgTitleStyle must be None when sgTitle is absent"
2498 );
2499 let json = serde_json::to_string(&snapshot.metadata).unwrap();
2500 assert!(
2501 !json.contains("sgTitleStyle"),
2502 "sgTitleStyle must not appear in serialized JSON when sgTitle is absent"
2503 );
2504 }
2505
2506 #[test]
2507 fn figure_scene_roundtrip_reconstructs_supported_plots() {
2508 let mut figure = Figure::new().with_title("Replay").with_subplot_grid(1, 2);
2509 figure.set_name("Roundtrip");
2510 figure.set_number_title(false);
2511 figure.set_visible(false);
2512 let mut line = LinePlot::new(vec![0.0, 1.0], vec![1.0, 2.0]).unwrap();
2513 line.label = Some("line".to_string());
2514 figure.add_line_plot_on_axes(line, 0);
2515 let mut scatter = ScatterPlot::new(vec![0.0, 1.0, 2.0], vec![2.0, 3.0, 4.0]).unwrap();
2516 scatter.label = Some("scatter".to_string());
2517 figure.add_scatter_plot_on_axes(scatter, 1);
2518
2519 let scene = FigureScene::capture(&figure);
2520 let rebuilt = scene.into_figure().expect("scene restore should succeed");
2521 assert_eq!(rebuilt.axes_grid(), (1, 2));
2522 assert_eq!(rebuilt.plots().count(), 2);
2523 assert_eq!(rebuilt.title.as_deref(), Some("Replay"));
2524 assert_eq!(rebuilt.name.as_deref(), Some("Roundtrip"));
2525 assert!(!rebuilt.number_title);
2526 assert!(!rebuilt.visible);
2527 }
2528
2529 #[test]
2530 fn figure_scene_roundtrip_reconstructs_patch() {
2531 let mut figure = Figure::new();
2532 let mut patch = PatchPlot::new(
2533 vec![
2534 Vec3::new(0.0, 0.0, 0.0),
2535 Vec3::new(1.0, 0.0, 0.0),
2536 Vec3::new(0.0, 1.0, 0.0),
2537 ],
2538 vec![vec![0, 1, 2]],
2539 )
2540 .unwrap();
2541 patch.set_label(Some("tri".into()));
2542 patch.set_force_3d(true);
2543 figure.add_patch_plot(patch);
2544
2545 let scene = FigureScene::capture(&figure);
2546 assert_eq!(scene.schema_version, FigureScene::SCHEMA_VERSION);
2547 assert!(matches!(scene.plots.first(), Some(ScenePlot::Patch { .. })));
2548 let rebuilt = scene.into_figure().expect("patch scene restore");
2549 let Some(PlotElement::Patch(patch)) = rebuilt.plots().next() else {
2550 panic!("expected patch plot");
2551 };
2552 assert_eq!(patch.faces(), &[vec![0, 1, 2]]);
2553 assert_eq!(patch.label(), Some("tri"));
2554 assert!(patch.force_3d());
2555 }
2556
2557 #[test]
2558 fn figure_scene_rejects_invalid_schema_versions() {
2559 let mut scene = FigureScene::capture(&Figure::new());
2560 scene.schema_version = 0;
2561 let err = scene.clone().into_figure().expect_err("schema 0 must fail");
2562 assert!(err.contains("unsupported figure scene schema version 0"));
2563
2564 scene.schema_version = FigureScene::SCHEMA_VERSION + 1;
2565 let err = scene.into_figure().expect_err("future schema must fail");
2566 assert!(err.contains(&format!(
2567 "unsupported figure scene schema version {}",
2568 FigureScene::SCHEMA_VERSION + 1
2569 )));
2570 }
2571
2572 #[test]
2573 fn figure_scene_rejects_patch_in_older_schema() {
2574 let mut figure = Figure::new();
2575 figure.add_patch_plot(
2576 PatchPlot::new(
2577 vec![
2578 Vec3::new(0.0, 0.0, 0.0),
2579 Vec3::new(1.0, 0.0, 0.0),
2580 Vec3::new(0.0, 1.0, 0.0),
2581 ],
2582 vec![vec![0, 1, 2]],
2583 )
2584 .unwrap(),
2585 );
2586
2587 let mut scene = FigureScene::capture(&figure);
2588 assert!(matches!(scene.plots.first(), Some(ScenePlot::Patch { .. })));
2589 scene.schema_version = 1;
2590
2591 let err = scene
2592 .into_figure()
2593 .expect_err("older patch schema must fail");
2594 assert!(err.contains("patch plots require figure scene schema version 2"));
2595 }
2596
2597 #[test]
2598 fn figure_scene_roundtrip_preserves_mesh_plot() {
2599 let mut figure = Figure::new();
2600 let mut mesh = MeshPlot::new(
2601 vec![
2602 Vec3::new(0.0, 0.0, 0.0),
2603 Vec3::new(1.0, 0.0, 0.0),
2604 Vec3::new(0.0, 1.0, 0.0),
2605 ],
2606 vec![[0, 1, 2]],
2607 )
2608 .unwrap();
2609 mesh.set_mesh_id(Some("mesh_1".to_string()));
2610 mesh.set_label(Some("mesh tri".to_string()));
2611 mesh.set_face_alpha(0.7);
2612 mesh.set_edge_width(0.25);
2613 mesh.set_edge_mode(MeshEdgeMode::Feature);
2614 mesh.set_feature_edge_groups(Some(vec![3]))
2615 .expect("feature group should be accepted");
2616 mesh.set_vertex_colors(Some(vec![
2617 Vec4::new(0.3, 0.4, 0.5, 1.0),
2618 Vec4::new(0.3, 0.4, 0.5, 1.0),
2619 Vec4::new(0.3, 0.4, 0.5, 1.0),
2620 ]))
2621 .expect("vertex colors should be accepted");
2622 mesh.set_triangle_colors(Some(vec![Vec4::new(0.3, 0.4, 0.5, 1.0)]))
2623 .expect("triangle color should be accepted");
2624 mesh.set_regions(vec![MeshRegion::new(
2625 "region_default",
2626 Some("Default Region".to_string()),
2627 Some("mesh_default".to_string()),
2628 vec![MeshTriangleRange::new(0, 1)],
2629 )]);
2630 mesh.set_highlighted_region_id(Some("region_default".to_string()));
2631 figure.add_mesh_plot(mesh);
2632
2633 let scene = FigureScene::capture(&figure);
2634 assert_eq!(scene.schema_version, FigureScene::SCHEMA_VERSION);
2635 assert!(matches!(scene.plots.first(), Some(ScenePlot::Mesh { .. })));
2636 let rebuilt = scene.into_figure().expect("mesh scene restore");
2637 let Some(PlotElement::Mesh(mesh)) = rebuilt.plots().next() else {
2638 panic!("expected mesh plot");
2639 };
2640 assert_eq!(mesh.mesh_id(), Some("mesh_1"));
2641 assert_eq!(mesh.triangles(), &[[0, 1, 2]]);
2642 assert_eq!(mesh.label(), Some("mesh tri"));
2643 assert!((mesh.face_alpha() - 0.7).abs() < f32::EPSILON);
2644 assert!((mesh.edge_width() - 0.25).abs() < f32::EPSILON);
2645 assert_eq!(mesh.edge_mode(), MeshEdgeMode::Feature);
2646 assert_eq!(mesh.feature_edge_groups().unwrap(), &[3]);
2647 assert_eq!(
2648 mesh.vertex_colors()
2649 .and_then(|colors| colors.first().copied()),
2650 Some(Vec4::new(0.3, 0.4, 0.5, 1.0))
2651 );
2652 assert_eq!(
2653 mesh.triangle_colors()
2654 .and_then(|colors| colors.first().copied()),
2655 Some(Vec4::new(0.3, 0.4, 0.5, 1.0))
2656 );
2657 assert_eq!(mesh.regions().len(), 1);
2658 assert_eq!(mesh.regions()[0].region_id, "region_default");
2659 assert_eq!(mesh.highlighted_region_id(), Some("region_default"));
2660 }
2661
2662 #[test]
2663 fn figure_scene_roundtrip_preserves_mesh_fea_overlays() {
2664 let mut figure = Figure::new();
2665 let mut mesh = MeshPlot::new(
2666 vec![
2667 Vec3::new(0.0, 0.0, 0.0),
2668 Vec3::new(1.0, 0.0, 0.0),
2669 Vec3::new(0.0, 1.0, 0.0),
2670 ],
2671 vec![[0, 1, 2]],
2672 )
2673 .unwrap();
2674 mesh.set_scalar_field(Some(MeshScalarField {
2675 field_id: "fea.structural.von_mises".to_string(),
2676 label: Some("Von Mises".to_string()),
2677 location: MeshFieldLocation::Vertex,
2678 values: vec![0.0, 0.5, 1.0],
2679 color_limits: Some([0.0, 1.0]),
2680 colormap: "viridis".to_string(),
2681 alpha: 0.8,
2682 }))
2683 .unwrap();
2684 mesh.set_vector_field(Some(MeshVectorField {
2685 field_id: "fea.em.flux_density".to_string(),
2686 label: Some("Flux density".to_string()),
2687 location: MeshFieldLocation::Triangle,
2688 vectors: vec![Vec3::new(0.0, 0.0, 1.0)],
2689 scale: 0.25,
2690 stride: 1,
2691 color: Vec4::new(0.9, 0.7, 0.2, 1.0),
2692 }))
2693 .unwrap();
2694 mesh.set_deformation(Some(MeshDeformation {
2695 field_id: "fea.structural.displacement".to_string(),
2696 label: Some("Displacement".to_string()),
2697 displacements: vec![Vec3::ZERO, Vec3::Z, Vec3::ZERO],
2698 scale: 0.5,
2699 }))
2700 .unwrap();
2701 figure.add_mesh_plot(mesh);
2702
2703 let rebuilt = FigureScene::capture(&figure)
2704 .into_figure()
2705 .expect("mesh scene restore");
2706 let Some(PlotElement::Mesh(mesh)) = rebuilt.plots().next() else {
2707 panic!("expected mesh plot");
2708 };
2709 assert_eq!(
2710 mesh.scalar_field().map(|field| field.field_id.as_str()),
2711 Some("fea.structural.von_mises")
2712 );
2713 assert_eq!(
2714 mesh.vector_field().map(|field| field.field_id.as_str()),
2715 Some("fea.em.flux_density")
2716 );
2717 assert_eq!(
2718 mesh.deformation().map(|field| field.field_id.as_str()),
2719 Some("fea.structural.displacement")
2720 );
2721 }
2722
2723 #[test]
2724 fn figure_scene_rejects_mesh_in_older_schema() {
2725 let mut figure = Figure::new();
2726 figure.add_mesh_plot(
2727 MeshPlot::new(
2728 vec![
2729 Vec3::new(0.0, 0.0, 0.0),
2730 Vec3::new(1.0, 0.0, 0.0),
2731 Vec3::new(0.0, 1.0, 0.0),
2732 ],
2733 vec![[0, 1, 2]],
2734 )
2735 .unwrap(),
2736 );
2737
2738 let mut scene = FigureScene::capture(&figure);
2739 assert!(matches!(scene.plots.first(), Some(ScenePlot::Mesh { .. })));
2740 scene.schema_version = 2;
2741
2742 let err = scene
2743 .into_figure()
2744 .expect_err("older mesh schema must fail");
2745 assert!(err.contains("mesh plots require figure scene schema version 3"));
2746 }
2747
2748 #[test]
2749 fn figure_scene_rejects_unknown_reference_line_orientation() {
2750 let mut scene = FigureScene::capture(&Figure::new());
2751 scene.plots.push(ScenePlot::ReferenceLine {
2752 orientation: "VERTICAL".into(),
2753 value: 2.0,
2754 color_rgba: [0.1, 0.2, 0.3, 1.0],
2755 line_width: 1.0,
2756 line_style: "Solid".into(),
2757 label: None,
2758 display_name: None,
2759 label_orientation: "horizontal".into(),
2760 axes_index: 0,
2761 visible: true,
2762 });
2763
2764 let rebuilt = scene.clone().into_figure().expect("valid orientation");
2765 let PlotElement::ReferenceLine(line) = rebuilt.plots().next().unwrap() else {
2766 panic!("expected reference line")
2767 };
2768 assert!(matches!(
2769 line.orientation,
2770 ReferenceLineOrientation::Vertical
2771 ));
2772
2773 let ScenePlot::ReferenceLine { orientation, .. } = &mut scene.plots[0] else {
2774 panic!("expected reference line scene plot")
2775 };
2776 *orientation = "diagonal".into();
2777
2778 let err = scene
2779 .into_figure()
2780 .expect_err("unknown orientation must fail");
2781 assert!(err.contains("unknown reference line orientation 'diagonal'"));
2782 }
2783
2784 #[test]
2785 fn figure_scene_roundtrip_reconstructs_surface_and_scatter3() {
2786 let mut figure = Figure::new().with_title("Replay3D").with_subplot_grid(1, 2);
2787 let mut surface = SurfacePlot::new(
2788 vec![0.0, 1.0],
2789 vec![0.0, 1.0],
2790 vec![vec![0.0, 1.0], vec![1.0, 2.0]],
2791 )
2792 .expect("surface data should be valid");
2793 surface.label = Some("surface".to_string());
2794 figure.add_surface_plot_on_axes(surface, 0);
2795
2796 let mut scatter3 = Scatter3Plot::new(vec![
2797 Vec3::new(0.0, 0.0, 0.0),
2798 Vec3::new(1.0, 2.0, 3.0),
2799 Vec3::new(2.0, 3.0, 4.0),
2800 ])
2801 .expect("scatter3 data should be valid");
2802 scatter3.label = Some("scatter3".to_string());
2803 figure.add_scatter3_plot_on_axes(scatter3, 1);
2804
2805 let scene = FigureScene::capture(&figure);
2806 let rebuilt = scene.into_figure().expect("scene restore should succeed");
2807 assert_eq!(rebuilt.axes_grid(), (1, 2));
2808 assert_eq!(rebuilt.plots().count(), 2);
2809 assert_eq!(rebuilt.title.as_deref(), Some("Replay3D"));
2810 assert!(matches!(
2811 rebuilt.plots().next(),
2812 Some(PlotElement::Surface(_))
2813 ));
2814 assert!(matches!(
2815 rebuilt.plots().nth(1),
2816 Some(PlotElement::Scatter3(_))
2817 ));
2818 }
2819
2820 #[test]
2821 fn figure_scene_roundtrip_preserves_line3_plot() {
2822 let mut figure = Figure::new();
2823 let line3 = Line3Plot::new(vec![0.0, 1.0], vec![1.0, 2.0], vec![2.0, 3.0])
2824 .unwrap()
2825 .with_label("Trajectory");
2826 figure.add_line3_plot(line3);
2827
2828 let rebuilt = FigureScene::capture(&figure)
2829 .into_figure()
2830 .expect("scene restore should succeed");
2831
2832 let PlotElement::Line3(line3) = rebuilt.plots().next().unwrap() else {
2833 panic!("expected line3")
2834 };
2835 assert_eq!(line3.x_data, vec![0.0, 1.0]);
2836 assert_eq!(line3.z_data, vec![2.0, 3.0]);
2837 assert_eq!(line3.label.as_deref(), Some("Trajectory"));
2838 }
2839
2840 #[test]
2841 fn figure_scene_roundtrip_preserves_contour_and_fill_plots() {
2842 let mut figure = Figure::new();
2843 let bounds = BoundingBox::new(Vec3::new(-1.0, -2.0, 0.0), Vec3::new(3.0, 4.0, 0.0));
2844 let vertices = vec![Vertex {
2845 position: [0.0, 0.0, 0.0],
2846 color: [1.0, 0.0, 0.0, 1.0],
2847 normal: [0.0, 0.0, 1.0],
2848 tex_coords: [0.0, 0.0],
2849 }];
2850 let fill = ContourFillPlot::from_vertices(vertices.clone(), bounds).with_label("fill");
2851 let contour = ContourPlot::from_vertices(vertices, 0.0, bounds)
2852 .with_label("lines")
2853 .with_line_width(2.0);
2854 figure.add_contour_fill_plot(fill);
2855 figure.add_contour_plot(contour);
2856
2857 let rebuilt = FigureScene::capture(&figure)
2858 .into_figure()
2859 .expect("scene restore should succeed");
2860 assert!(matches!(
2861 rebuilt.plots().next(),
2862 Some(PlotElement::ContourFill(_))
2863 ));
2864 let Some(PlotElement::Contour(contour)) = rebuilt.plots().nth(1) else {
2865 panic!("expected contour")
2866 };
2867 assert_eq!(contour.line_width, 2.0);
2868 }
2869
2870 #[test]
2871 fn figure_scene_roundtrip_preserves_stem_style_surface() {
2872 let mut figure = Figure::new();
2873 let mut stem = StemPlot::new(vec![0.0, 1.0], vec![1.0, 2.0])
2874 .unwrap()
2875 .with_style(
2876 Vec4::new(1.0, 0.0, 0.0, 1.0),
2877 2.0,
2878 crate::plots::line::LineStyle::Dashed,
2879 -1.0,
2880 )
2881 .with_baseline_style(Vec4::new(0.0, 0.0, 0.0, 1.0), false)
2882 .with_label("Impulse");
2883 stem.set_marker(Some(crate::plots::line::LineMarkerAppearance {
2884 kind: crate::plots::scatter::MarkerStyle::Square,
2885 size: 8.0,
2886 edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
2887 face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
2888 filled: true,
2889 }));
2890 figure.add_stem_plot(stem);
2891
2892 let rebuilt = FigureScene::capture(&figure)
2893 .into_figure()
2894 .expect("scene restore should succeed");
2895 let PlotElement::Stem(stem) = rebuilt.plots().next().unwrap() else {
2896 panic!("expected stem")
2897 };
2898 assert_eq!(stem.baseline, -1.0);
2899 assert_eq!(stem.line_width, 2.0);
2900 assert_eq!(stem.label.as_deref(), Some("Impulse"));
2901 assert!(!stem.baseline_visible);
2902 assert!(stem.marker.as_ref().map(|m| m.filled).unwrap_or(false));
2903 assert_eq!(stem.marker.as_ref().map(|m| m.size), Some(8.0));
2904 }
2905
2906 #[test]
2907 fn figure_scene_roundtrip_preserves_bar_plot() {
2908 let mut figure = Figure::new();
2909 let bar = BarChart::new(vec!["A".into(), "B".into()], vec![2.0, 3.5])
2910 .unwrap()
2911 .with_style(Vec4::new(0.2, 0.4, 0.8, 1.0), 0.95)
2912 .with_outline(Vec4::new(0.1, 0.1, 0.1, 1.0), 1.5)
2913 .with_label("Histogram")
2914 .with_stack_offsets(vec![1.0, 0.5]);
2915 figure.add_bar_chart(bar);
2916
2917 let rebuilt = FigureScene::capture(&figure)
2918 .into_figure()
2919 .expect("scene restore should succeed");
2920 let PlotElement::Bar(bar) = rebuilt.plots().next().unwrap() else {
2921 panic!("expected bar")
2922 };
2923 assert_eq!(bar.labels, vec!["A", "B"]);
2924 assert_eq!(bar.values().unwrap_or(&[]), &[2.0, 3.5]);
2925 assert_eq!(bar.bar_width, 0.95);
2926 assert_eq!(bar.outline_width, 1.5);
2927 assert_eq!(bar.label.as_deref(), Some("Histogram"));
2928 assert_eq!(bar.stack_offsets().unwrap_or(&[]), &[1.0, 0.5]);
2929 assert!(bar.histogram_bin_edges().is_none());
2930 }
2931
2932 #[test]
2933 fn figure_scene_roundtrip_preserves_histogram_bin_edges() {
2934 let mut figure = Figure::new();
2935 let mut bar = BarChart::new(vec!["bin1".into(), "bin2".into()], vec![4.0, 5.0]).unwrap();
2936 bar.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
2937 figure.add_bar_chart(bar);
2938
2939 let rebuilt = FigureScene::capture(&figure)
2940 .into_figure()
2941 .expect("scene restore should succeed");
2942 let PlotElement::Bar(bar) = rebuilt.plots().next().unwrap() else {
2943 panic!("expected bar")
2944 };
2945 assert_eq!(bar.histogram_bin_edges().unwrap_or(&[]), &[0.0, 0.5, 1.0]);
2946 }
2947
2948 #[test]
2949 fn figure_scene_roundtrip_preserves_errorbar_style_surface() {
2950 let mut figure = Figure::new();
2951 let mut error = ErrorBar::new_vertical(
2952 vec![0.0, 1.0],
2953 vec![1.0, 2.0],
2954 vec![0.1, 0.2],
2955 vec![0.2, 0.3],
2956 )
2957 .unwrap()
2958 .with_style(
2959 Vec4::new(1.0, 0.0, 0.0, 1.0),
2960 2.0,
2961 crate::plots::line::LineStyle::Dashed,
2962 10.0,
2963 )
2964 .with_label("Err");
2965 error.set_marker(Some(crate::plots::line::LineMarkerAppearance {
2966 kind: crate::plots::scatter::MarkerStyle::Triangle,
2967 size: 8.0,
2968 edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
2969 face_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
2970 filled: true,
2971 }));
2972 figure.add_errorbar(error);
2973
2974 let rebuilt = FigureScene::capture(&figure)
2975 .into_figure()
2976 .expect("scene restore should succeed");
2977 let PlotElement::ErrorBar(error) = rebuilt.plots().next().unwrap() else {
2978 panic!("expected errorbar")
2979 };
2980 assert_eq!(error.line_width, 2.0);
2981 assert_eq!(error.cap_size, 10.0);
2982 assert_eq!(error.label.as_deref(), Some("Err"));
2983 assert_eq!(error.line_style, crate::plots::line::LineStyle::Dashed);
2984 assert!(error.marker.as_ref().map(|m| m.filled).unwrap_or(false));
2985 }
2986
2987 #[test]
2988 fn figure_scene_roundtrip_preserves_errorbar_both_direction() {
2989 let mut figure = Figure::new();
2990 let error = ErrorBar::new_both(
2991 vec![1.0, 2.0],
2992 vec![3.0, 4.0],
2993 vec![0.1, 0.2],
2994 vec![0.2, 0.3],
2995 vec![0.3, 0.4],
2996 vec![0.4, 0.5],
2997 )
2998 .unwrap();
2999 figure.add_errorbar(error);
3000 let rebuilt = FigureScene::capture(&figure)
3001 .into_figure()
3002 .expect("scene restore should succeed");
3003 let PlotElement::ErrorBar(error) = rebuilt.plots().next().unwrap() else {
3004 panic!("expected errorbar")
3005 };
3006 assert_eq!(
3007 error.orientation,
3008 crate::plots::errorbar::ErrorBarOrientation::Both
3009 );
3010 assert_eq!(error.x_neg, vec![0.1, 0.2]);
3011 assert_eq!(error.x_pos, vec![0.2, 0.3]);
3012 }
3013
3014 #[test]
3015 fn figure_scene_roundtrip_preserves_quiver_plot() {
3016 let mut figure = Figure::new();
3017 let quiver = QuiverPlot::new(
3018 vec![0.0, 1.0],
3019 vec![1.0, 2.0],
3020 vec![0.5, -0.5],
3021 vec![1.0, 0.25],
3022 )
3023 .unwrap()
3024 .with_style(Vec4::new(0.2, 0.3, 0.4, 1.0), 2.0, 1.5, 0.2)
3025 .with_label("Field");
3026 figure.add_quiver_plot(quiver);
3027
3028 let rebuilt = FigureScene::capture(&figure)
3029 .into_figure()
3030 .expect("scene restore should succeed");
3031 let PlotElement::Quiver(quiver) = rebuilt.plots().next().unwrap() else {
3032 panic!("expected quiver")
3033 };
3034 assert_eq!(quiver.u, vec![0.5, -0.5]);
3035 assert_eq!(quiver.v, vec![1.0, 0.25]);
3036 assert_eq!(quiver.line_width, 2.0);
3037 assert_eq!(quiver.scale, 1.5);
3038 assert_eq!(quiver.head_size, 0.2);
3039 assert_eq!(quiver.label.as_deref(), Some("Field"));
3040 }
3041
3042 #[test]
3043 fn figure_scene_roundtrip_preserves_image_surface_mode_and_color_grid() {
3044 let mut figure = Figure::new();
3045 let surface = SurfacePlot::new(
3046 vec![0.0, 1.0],
3047 vec![0.0, 1.0],
3048 vec![vec![0.0, 0.0], vec![0.0, 0.0]],
3049 )
3050 .unwrap()
3051 .with_flatten_z(true)
3052 .with_image_mode(true)
3053 .with_color_grid(vec![
3054 vec![Vec4::new(1.0, 0.0, 0.0, 1.0), Vec4::new(0.0, 1.0, 0.0, 1.0)],
3055 vec![Vec4::new(0.0, 0.0, 1.0, 1.0), Vec4::new(1.0, 1.0, 1.0, 1.0)],
3056 ]);
3057 figure.add_surface_plot(surface);
3058
3059 let rebuilt = FigureScene::capture(&figure)
3060 .into_figure()
3061 .expect("scene restore should succeed");
3062 let PlotElement::Surface(surface) = rebuilt.plots().next().unwrap() else {
3063 panic!("expected surface")
3064 };
3065 assert!(surface.flatten_z);
3066 assert!(surface.image_mode);
3067 assert!(surface.color_grid.is_some());
3068 assert_eq!(
3069 surface.color_grid.as_ref().unwrap()[0][0],
3070 Vec4::new(1.0, 0.0, 0.0, 1.0)
3071 );
3072 }
3073
3074 #[test]
3075 fn figure_scene_roundtrip_preserves_area_lower_curve() {
3076 let mut figure = Figure::new();
3077 let area = AreaPlot::new(vec![1.0, 2.0], vec![2.0, 3.0])
3078 .unwrap()
3079 .with_lower_curve(vec![0.5, 1.0])
3080 .with_label("Stacked");
3081 figure.add_area_plot(area);
3082
3083 let rebuilt = FigureScene::capture(&figure)
3084 .into_figure()
3085 .expect("scene restore should succeed");
3086 let PlotElement::Area(area) = rebuilt.plots().next().unwrap() else {
3087 panic!("expected area")
3088 };
3089 assert_eq!(area.lower_y, Some(vec![0.5, 1.0]));
3090 assert_eq!(area.label.as_deref(), Some("Stacked"));
3091 }
3092
3093 #[test]
3094 fn figure_scene_roundtrip_preserves_axes_local_limits_and_colormap_state() {
3095 let mut figure = Figure::new().with_subplot_grid(1, 2);
3096 figure.set_axes_limits(1, Some((1.0, 2.0)), Some((3.0, 4.0)));
3097 figure.set_axes_z_limits(1, Some((5.0, 6.0)));
3098 figure.set_axes_grid_enabled(1, false);
3099 figure.set_axes_minor_grid_enabled(1, true);
3100 figure.set_axes_box_enabled(1, false);
3101 figure.set_axes_axis_equal(1, true);
3102 figure.set_axes_kind(1, AxesKind::Polar);
3103 figure.set_axes_colorbar_enabled(1, true);
3104 figure.set_axes_colormap(1, ColorMap::Hot);
3105 figure.set_axes_color_limits(1, Some((0.0, 10.0)));
3106 figure.set_axes_style(
3107 1,
3108 TextStyle {
3109 font_size: Some(14.0),
3110 ..Default::default()
3111 },
3112 );
3113 figure.set_active_axes_index(1);
3114
3115 let rebuilt = FigureScene::capture(&figure)
3116 .into_figure()
3117 .expect("scene restore should succeed");
3118 let meta = rebuilt.axes_metadata(1).unwrap();
3119 assert_eq!(meta.x_limits, Some((1.0, 2.0)));
3120 assert_eq!(meta.y_limits, Some((3.0, 4.0)));
3121 assert_eq!(meta.z_limits, Some((5.0, 6.0)));
3122 assert!(!meta.grid_enabled);
3123 assert!(meta.minor_grid_enabled);
3124 assert!(meta.minor_grid_explicit);
3125 assert!(!meta.box_enabled);
3126 assert!(meta.axis_equal);
3127 assert_eq!(meta.axes_kind, AxesKind::Polar);
3128 assert!(meta.colorbar_enabled);
3129 assert_eq!(format!("{:?}", meta.colormap), "Hot");
3130 assert_eq!(meta.color_limits, Some((0.0, 10.0)));
3131 assert_eq!(meta.axes_style.font_size, Some(14.0));
3132 }
3133
3134 #[test]
3135 fn axes_metadata_deserializes_without_axes_style() {
3136 let json = r#"{
3137 "legendEnabled": true,
3138 "colormap": "Parula",
3139 "titleStyle": {"visible": true},
3140 "xLabelStyle": {"visible": true},
3141 "yLabelStyle": {"visible": true},
3142 "zLabelStyle": {"visible": true},
3143 "legendStyle": {"visible": true}
3144 }"#;
3145 let serialized: SerializedAxesMetadata = serde_json::from_str(json).unwrap();
3146 let metadata = AxesMetadata::from(serialized);
3147 assert!(metadata.axes_style.color.is_none());
3148 assert!(metadata.axes_style.font_size.is_none());
3149 assert!(metadata.axes_style.font_weight.is_none());
3150 assert!(metadata.axes_style.font_angle.is_none());
3151 assert!(metadata.axes_style.interpreter.is_none());
3152 assert!(metadata.axes_style.visible);
3153 }
3154
3155 #[test]
3156 fn figure_scene_roundtrip_preserves_axes_local_annotation_metadata() {
3157 let mut figure = Figure::new().with_subplot_grid(1, 2);
3158 figure.set_sg_title("All Panels");
3159 figure.set_sg_title_style(TextStyle {
3160 font_weight: Some("bold".into()),
3161 font_size: Some(20.0),
3162 ..Default::default()
3163 });
3164 figure.set_active_axes_index(0);
3165 figure.set_axes_title(0, "Left");
3166 figure.set_axes_xlabel(0, "LX");
3167 figure.set_axes_ylabel(0, "LY");
3168 figure.set_axes_legend_enabled(0, false);
3169 figure.set_axes_title(1, "Right");
3170 figure.set_axes_xlabel(1, "RX");
3171 figure.set_axes_ylabel(1, "RY");
3172 figure.set_axes_legend_enabled(1, true);
3173 figure.set_axes_legend_style(
3174 1,
3175 LegendStyle {
3176 location: Some("northeast".into()),
3177 font_weight: Some("bold".into()),
3178 orientation: Some("horizontal".into()),
3179 ..Default::default()
3180 },
3181 );
3182 if let Some(meta) = figure.axes_metadata.get_mut(0) {
3183 meta.title_style.font_weight = Some("bold".into());
3184 meta.title_style.font_angle = Some("italic".into());
3185 }
3186 figure.set_active_axes_index(1);
3187
3188 let rebuilt = FigureScene::capture(&figure)
3189 .into_figure()
3190 .expect("scene restore should succeed");
3191
3192 assert_eq!(rebuilt.active_axes_index, 1);
3193 assert_eq!(rebuilt.sg_title.as_deref(), Some("All Panels"));
3194 assert_eq!(rebuilt.sg_title_style.font_weight.as_deref(), Some("bold"));
3195 assert_eq!(rebuilt.sg_title_style.font_size, Some(20.0));
3196 assert_eq!(
3197 rebuilt.axes_metadata(0).and_then(|m| m.title.as_deref()),
3198 Some("Left")
3199 );
3200 assert_eq!(
3201 rebuilt.axes_metadata(0).and_then(|m| m.x_label.as_deref()),
3202 Some("LX")
3203 );
3204 assert_eq!(
3205 rebuilt.axes_metadata(0).and_then(|m| m.y_label.as_deref()),
3206 Some("LY")
3207 );
3208 assert!(!rebuilt.axes_metadata(0).unwrap().legend_enabled);
3209 assert_eq!(
3210 rebuilt
3211 .axes_metadata(0)
3212 .unwrap()
3213 .title_style
3214 .font_weight
3215 .as_deref(),
3216 Some("bold")
3217 );
3218 assert_eq!(
3219 rebuilt
3220 .axes_metadata(0)
3221 .unwrap()
3222 .title_style
3223 .font_angle
3224 .as_deref(),
3225 Some("italic")
3226 );
3227 assert_eq!(
3228 rebuilt.axes_metadata(1).and_then(|m| m.title.as_deref()),
3229 Some("Right")
3230 );
3231 assert_eq!(
3232 rebuilt.axes_metadata(1).and_then(|m| m.x_label.as_deref()),
3233 Some("RX")
3234 );
3235 assert_eq!(
3236 rebuilt.axes_metadata(1).and_then(|m| m.y_label.as_deref()),
3237 Some("RY")
3238 );
3239 assert_eq!(
3240 rebuilt
3241 .axes_metadata(1)
3242 .unwrap()
3243 .legend_style
3244 .location
3245 .as_deref(),
3246 Some("northeast")
3247 );
3248 assert_eq!(
3249 rebuilt
3250 .axes_metadata(1)
3251 .unwrap()
3252 .legend_style
3253 .font_weight
3254 .as_deref(),
3255 Some("bold")
3256 );
3257 assert_eq!(
3258 rebuilt
3259 .axes_metadata(1)
3260 .unwrap()
3261 .legend_style
3262 .orientation
3263 .as_deref(),
3264 Some("horizontal")
3265 );
3266 }
3267
3268 #[test]
3269 fn figure_scene_roundtrip_preserves_axes_local_log_modes() {
3270 let mut figure = Figure::new().with_subplot_grid(1, 2);
3271 figure.set_axes_log_modes(0, true, false);
3272 figure.set_axes_log_modes(1, false, true);
3273 figure.set_active_axes_index(1);
3274
3275 let rebuilt = FigureScene::capture(&figure)
3276 .into_figure()
3277 .expect("scene restore should succeed");
3278
3279 assert!(rebuilt.axes_metadata(0).unwrap().x_log);
3280 assert!(!rebuilt.axes_metadata(0).unwrap().y_log);
3281 assert!(!rebuilt.axes_metadata(1).unwrap().x_log);
3282 assert!(rebuilt.axes_metadata(1).unwrap().y_log);
3283 assert!(!rebuilt.x_log);
3284 assert!(rebuilt.y_log);
3285 }
3286
3287 #[test]
3288 fn figure_scene_roundtrip_preserves_zlabel_and_view_state() {
3289 let mut figure = Figure::new().with_subplot_grid(1, 2);
3290 figure.set_axes_zlabel(1, "Height");
3291 figure.set_axes_view(1, 45.0, 20.0);
3292 figure.set_active_axes_index(1);
3293
3294 let rebuilt = FigureScene::capture(&figure)
3295 .into_figure()
3296 .expect("scene restore should succeed");
3297
3298 assert_eq!(
3299 rebuilt.axes_metadata(1).unwrap().z_label.as_deref(),
3300 Some("Height")
3301 );
3302 assert_eq!(
3303 rebuilt.axes_metadata(1).unwrap().view_azimuth_deg,
3304 Some(45.0)
3305 );
3306 assert_eq!(
3307 rebuilt.axes_metadata(1).unwrap().view_elevation_deg,
3308 Some(20.0)
3309 );
3310 assert_eq!(rebuilt.z_label.as_deref(), Some("Height"));
3311 }
3312
3313 #[test]
3314 fn figure_scene_roundtrip_preserves_pie_metadata() {
3315 let mut figure = Figure::new();
3316 let pie = crate::plots::PieChart::new(vec![1.0, 2.0], None)
3317 .unwrap()
3318 .with_slice_labels(vec!["A".into(), "B".into()])
3319 .with_explode(vec![false, true]);
3320 figure.add_pie_chart(pie);
3321
3322 let rebuilt = FigureScene::capture(&figure)
3323 .into_figure()
3324 .expect("scene restore should succeed");
3325 let crate::plots::figure::PlotElement::Pie(pie) = rebuilt.plots().next().unwrap() else {
3326 panic!("expected pie")
3327 };
3328 assert_eq!(pie.slice_labels, vec!["A", "B"]);
3329 assert_eq!(pie.explode, vec![false, true]);
3330 }
3331
3332 #[test]
3333 fn scene_plot_deserialize_maps_null_numeric_values_to_nan() {
3334 let json = r#"{
3335 "schemaVersion": 1,
3336 "layout": { "axesRows": 1, "axesCols": 1, "axesIndices": [0] },
3337 "metadata": {
3338 "gridEnabled": true,
3339 "legendEnabled": false,
3340 "colorbarEnabled": false,
3341 "axisEqual": false,
3342 "backgroundRgba": [1,1,1,1],
3343 "legendEntries": []
3344 },
3345 "plots": [
3346 {
3347 "kind": "surface",
3348 "x": [0.0, null],
3349 "y": [0.0, 1.0],
3350 "z": [[0.0, null], [1.0, 2.0]],
3351 "colormap": "Parula",
3352 "shading_mode": "Smooth",
3353 "wireframe": false,
3354 "alpha": 1.0,
3355 "flatten_z": false,
3356 "color_limits": null,
3357 "axes_index": 0,
3358 "label": null,
3359 "visible": true
3360 }
3361 ]
3362 }"#;
3363 let scene: FigureScene = serde_json::from_str(json).expect("scene should deserialize");
3364 let ScenePlot::Surface { x, z, .. } = &scene.plots[0] else {
3365 panic!("expected surface plot");
3366 };
3367 assert!(x[1].is_nan());
3368 assert!(z[0][1].is_nan());
3369 }
3370
3371 #[test]
3372 fn scene_plot_deserialize_maps_null_scatter3_components_to_nan() {
3373 let json = r#"{
3374 "schemaVersion": 1,
3375 "layout": { "axesRows": 1, "axesCols": 1, "axesIndices": [0] },
3376 "metadata": {
3377 "gridEnabled": true,
3378 "legendEnabled": false,
3379 "colorbarEnabled": false,
3380 "axisEqual": false,
3381 "backgroundRgba": [1,1,1,1],
3382 "legendEntries": []
3383 },
3384 "plots": [
3385 {
3386 "kind": "scatter3",
3387 "points": [[0.0, 1.0, null], [1.0, null, 2.0]],
3388 "colors_rgba": [[0.2, 0.4, 0.6, 1.0], [0.1, 0.2, 0.3, 1.0]],
3389 "point_size": 6.0,
3390 "point_sizes": [3.0, null],
3391 "axes_index": 0,
3392 "label": null,
3393 "visible": true
3394 }
3395 ]
3396 }"#;
3397 let scene: FigureScene = serde_json::from_str(json).expect("scene should deserialize");
3398 let ScenePlot::Scatter3 {
3399 points,
3400 point_sizes,
3401 ..
3402 } = &scene.plots[0]
3403 else {
3404 panic!("expected scatter3 plot");
3405 };
3406 assert!(points[0][2].is_nan());
3407 assert!(points[1][1].is_nan());
3408 assert!(point_sizes.as_ref().unwrap()[1].is_nan());
3409 }
3410}