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