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