Skip to main content

ferritin_molviewspec/molviewspec/
nodes.rs

1//! This module has functionality to build up MolViewSpec.
2//!
3//! MolViewSpec (MVS) is a lightweight, JSON-based specification for describing molecular structures and their visual representation.
4//! It is designed to be human-readable, easily shareable, and compatible with various molecular visualization tools.
5//!
6//! - MolViewSpec GitHub repository: <https://github.com/molstar/mol-view-spec>
7//! - MolViewSpec documentation: <https://molstar.org/viewer/molviewspec/>
8//! - MolStar Viewer (which supports MVS): <https://molstar.org/viewer/>
9//!
10//! We try to adhere very closely to the python library API. Almost all the action happens
11//! on the `Nodes`. Because we are building a nested tree of data, we need most of the parts to
12//! be mutable and you can see almost all the function calls:
13//!
14//! 1. check the type and the params and act only on correct parent nodes
15//! 2. because of 1, we return `Option<>`
16//! 3. If a node is returned is it `&mut Node`
17//!
18//!
19use chrono::Utc;
20use serde::{Deserialize, Deserializer, Serialize};
21use serde_json;
22use std::io::Read;
23use urlencoding;
24use validator::Validate;
25
26/// Tolerance used by [`is_rotation_matrix`], matching the Python builder.
27const ROTATION_DET_EPS: f64 = 0.005;
28
29/// Whether `t` is a 3x3 rotation matrix: nine values whose determinant has
30/// magnitude 1 (within 0.005).
31///
32/// Mirrors `_is_rotation_matrix` in the Python mol-view-spec builder, including
33/// its use of `|det|`. The determinant is the same for row- and column-major
34/// order, so either layout is accepted.
35pub fn is_rotation_matrix(t: &[f64]) -> bool {
36    let &[a00, a10, a20, a01, a11, a21, a02, a12, a22] = t else {
37        return false;
38    };
39    let det = a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20)
40        + a02 * (a10 * a21 - a11 * a20);
41    (det.abs() - 1.0).abs() <= ROTATION_DET_EPS
42}
43
44// KindT
45//
46// Enum of node types corresponding to the MolViewSpec Nodes
47//
48#[derive(PartialEq, Serialize, Deserialize, Debug, Default, Clone)]
49#[serde(rename_all = "snake_case")]
50pub enum KindT {
51    #[default]
52    Root,
53    Camera,
54    Canvas,
55    Color,
56    ColorFromSource,
57    ColorFromUri,
58    Component,
59    ComponentFromSource,
60    ComponentFromUri,
61    Download,
62    Focus,
63    GenericVisuals,
64    Label,
65    LabelFromSource,
66    LabelFromUri,
67    Line,
68    Parse,
69    Representation,
70    Sphere,
71    Structure,
72    Tooltip,
73    TooltipFromSource,
74    TooltipFromUri,
75    Transform,
76    Volume,
77    VolumeRepresentation,
78}
79
80// NodeParams
81//
82// Enum of params per node type. Each of the variants is typed.
83//
84#[derive(Serialize, Deserialize, Debug, Clone)]
85#[serde(untagged)]
86pub enum NodeParams {
87    DownloadParams(DownloadParams),
88    ParseParams(ParseParams),
89    StructureParams(StructureParams),
90    RepresentationParams(RepresentationParams),
91    DataFromUriParams(DataFromUriParams),
92    DataFromSourceParams(DataFromSourceParams),
93    ComponentInlineParams(ComponentInlineParams),
94    ComponentFromUriParams(ComponentFromUriParams),
95    ComponentFromSourceParams(ComponentFromSourceParams),
96    ColorInlineParams(ColorInlineParams),
97    ColorFromUriParams(ColorFromUriParams),
98    ColorFromSourceParams(ColorFromSourceParams),
99    LabelInlineParams(LabelInlineParams),
100    LabelFromUriParams(LabelFromUriParams),
101    LabelFromSourceParams(LabelFromSourceParams),
102    TooltipInlineParams(TooltipInlineParams),
103    TooltipFromUriParams(TooltipFromUriParams),
104    TooltipFromSourceParams(TooltipFromSourceParams),
105    FocusInlineParams(FocusInlineParams),
106    TransformParams(TransformParams),
107    CameraParams(CameraParams),
108    CanvasParams(CanvasParams),
109    SphereParams(SphereParams),
110    LineParams(LineParams),
111    VolumeParams(VolumeParams),
112    VolumeRepresentationParams(VolumeRepresentationParams),
113}
114
115// The spec treats leaf nodes as having no children key; an empty array from a
116// hand-crafted or third-party Node should not be observable — normalize on deser.
117fn deserialize_children<'de, D>(deserializer: D) -> Result<Option<Vec<Node>>, D::Error>
118where
119    D: serde::Deserializer<'de>,
120{
121    let opt = Option::<Vec<Node>>::deserialize(deserializer)?;
122    Ok(opt.filter(|v| !v.is_empty()))
123}
124
125/// Node
126///
127/// This is the core data structure for generating MSVJ files. Each node type can have a type, params, and children.
128///
129/// Methods derived from the Python API found [here](https://github.com/molstar/mol-view-spec/blob/master/molviewspec/molviewspec/builder.py)
130///
131#[derive(Serialize, Debug, Default, Clone)]
132pub struct Node {
133    pub kind: KindT,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub params: Option<NodeParams>,
136    #[serde(
137        skip_serializing_if = "Option::is_none",
138        deserialize_with = "deserialize_children",
139        default
140    )]
141    pub children: Option<Vec<Node>>,
142}
143impl Node {
144    // Common to All Nodes
145    pub fn new(kind: KindT, params: Option<NodeParams>) -> Node {
146        Node {
147            kind,
148            params,
149            children: None,
150        }
151    }
152    pub fn add_child(&mut self, node: Node) {
153        match &mut self.children {
154            Some(children) => children.push(node),
155            None => self.children = Some(vec![node]),
156        }
157    }
158    pub fn get_kind(&self) -> &KindT {
159        &self.kind
160    }
161    /// Create the download node
162    pub fn download(&mut self, url: &str) -> Option<&mut Node> {
163        if self.kind == KindT::Root {
164            let url = url.to_string();
165            let download_node = Node::new(
166                KindT::Download,
167                Some(NodeParams::DownloadParams(DownloadParams { url })),
168            );
169
170            self.children
171                .get_or_insert_with(Vec::new)
172                .push(download_node);
173            self.children.as_mut().unwrap().last_mut()
174        } else {
175            None
176        }
177    }
178    /// Parse a Download Node
179    pub fn parse(&mut self, params: ParseParams) -> Option<&mut Node> {
180        if self.kind == KindT::Download {
181            let parse_node = Node::new(KindT::Parse, Some(NodeParams::ParseParams(params)));
182            self.children.get_or_insert_with(Vec::new).push(parse_node);
183            self.children.as_mut().unwrap().last_mut()
184        } else {
185            None
186        }
187    }
188
189    // Parse methods ------------------------------------------------------
190
191    /// Create a structure for the deposited (asymmetric unit / model) coordinates.
192    pub fn model_structure(&mut self, params: StructureParams) -> Option<&mut Node> {
193        if self.kind == KindT::Parse {
194            let struct_node =
195                Node::new(KindT::Structure, Some(NodeParams::StructureParams(params)));
196            self.children.get_or_insert_with(Vec::new).push(struct_node);
197            self.children.as_mut().unwrap().last_mut()
198        } else {
199            None
200        }
201    }
202    /// Parse a Download Node
203    pub fn assembly_structure(&mut self, params: StructureParams) -> Option<&mut Node> {
204        if self.kind == KindT::Parse {
205            let struct_node =
206                Node::new(KindT::Structure, Some(NodeParams::StructureParams(params)));
207            self.children.get_or_insert_with(Vec::new).push(struct_node);
208            self.children.as_mut().unwrap().last_mut()
209        } else {
210            None
211        }
212    }
213    /// Symmetry Structure
214    pub fn symmetry_structure(&mut self, params: StructureParams) -> Option<&mut Node> {
215        // todo: this is the same as the regular structure bit above......
216        if self.kind == KindT::Parse {
217            let struct_node =
218                Node::new(KindT::Structure, Some(NodeParams::StructureParams(params)));
219            self.children.get_or_insert_with(Vec::new).push(struct_node);
220            self.children.as_mut().unwrap().last_mut()
221        } else {
222            None
223        }
224    }
225
226    /// Parse a Download Node
227    pub fn symmetry_mates_structure(&mut self, params: StructureParams) -> Option<&mut Node> {
228        // todo: this is the same as the regular structure bit above......
229        if self.kind == KindT::Parse {
230            let struct_node =
231                Node::new(KindT::Structure, Some(NodeParams::StructureParams(params)));
232            self.children.get_or_insert_with(Vec::new).push(struct_node);
233            self.children.as_mut().unwrap().last_mut()
234        } else {
235            None
236        }
237    }
238
239    /// Create a Volume node (parallel to a Structure node — loads volumetric data).
240    pub fn volume(&mut self, params: VolumeParams) -> Option<&mut Node> {
241        if self.kind == KindT::Parse {
242            let volume_node = Node::new(KindT::Volume, Some(NodeParams::VolumeParams(params)));
243            self.children.get_or_insert_with(Vec::new).push(volume_node);
244            self.children.as_mut().unwrap().last_mut()
245        } else {
246            None
247        }
248    }
249
250    // Volume methods ------------------------------------------------------
251
252    /// Add an isosurface or grid-slice representation for this volume.
253    pub fn volume_representation(
254        &mut self,
255        params: VolumeRepresentationParams,
256    ) -> Option<&mut Node> {
257        if self.kind == KindT::Volume {
258            let node = Node::new(
259                KindT::VolumeRepresentation,
260                Some(NodeParams::VolumeRepresentationParams(params)),
261            );
262            self.children.get_or_insert_with(Vec::new).push(node);
263            self.children.as_mut().unwrap().last_mut()
264        } else {
265            None
266        }
267    }
268
269    // Structure methods ------------------------------------------------------
270
271    /// Create a Component
272    pub fn component(&mut self, selector: ComponentSelector) -> Option<&mut Node> {
273        if self.kind == KindT::Structure {
274            let component_node = match selector {
275                ComponentSelector::Selector(sel) => Node::new(
276                    KindT::Component,
277                    Some(NodeParams::ComponentInlineParams(ComponentInlineParams {
278                        selector: ComponentSelector::Selector(sel),
279                    })),
280                ),
281                // DECISION (matches Python mol-view-spec behavior): a ComponentExpression
282                // with ALL fields set to None means "match everything" (all-true mask).
283                // It does NOT mean "match nothing". Phase 2 selector evaluation must
284                // return an all-true AtomMask when every field of the Expression is None.
285                // See test_mask_empty_expression_all_fields_none (T2-21).
286                ComponentSelector::Expression(expr) => Node::new(
287                    KindT::Component,
288                    Some(NodeParams::ComponentInlineParams(ComponentInlineParams {
289                        selector: ComponentSelector::Expression(expr),
290                    })),
291                ),
292                ComponentSelector::ExpressionList(expr_list) => Node::new(
293                    KindT::Component,
294                    Some(NodeParams::ComponentInlineParams(ComponentInlineParams {
295                        selector: ComponentSelector::ExpressionList(expr_list),
296                    })),
297                ),
298            };
299            self.children
300                .get_or_insert_with(Vec::new)
301                .push(component_node);
302            self.children.as_mut().unwrap().last_mut()
303        } else {
304            None
305        }
306    }
307    pub fn component_from_uri(&mut self, params: ComponentFromUriParams) -> Option<&mut Node> {
308        if self.kind == KindT::Structure {
309            let node = Node::new(
310                KindT::ComponentFromUri,
311                Some(NodeParams::ComponentFromUriParams(params)),
312            );
313            self.children.get_or_insert_with(Vec::new).push(node);
314            self.children.as_mut().unwrap().last_mut()
315        } else {
316            None
317        }
318    }
319    pub fn component_from_source(
320        &mut self,
321        params: ComponentFromSourceParams,
322    ) -> Option<&mut Node> {
323        if self.kind == KindT::Structure {
324            let node = Node::new(
325                KindT::ComponentFromSource,
326                Some(NodeParams::ComponentFromSourceParams(params)),
327            );
328            self.children.get_or_insert_with(Vec::new).push(node);
329            self.children.as_mut().unwrap().last_mut()
330        } else {
331            None
332        }
333    }
334    /// Add labels loaded from an external annotation file.
335    pub fn label_from_uri(&mut self, params: LabelFromUriParams) -> Option<&mut Node> {
336        self.push_structure_child(KindT::LabelFromUri, NodeParams::LabelFromUriParams(params))
337    }
338
339    /// Add labels read from a category of the structure's own source file.
340    pub fn label_from_source(&mut self, params: LabelFromSourceParams) -> Option<&mut Node> {
341        self.push_structure_child(
342            KindT::LabelFromSource,
343            NodeParams::LabelFromSourceParams(params),
344        )
345    }
346
347    /// Add tooltips loaded from an external annotation file.
348    pub fn tooltip_from_uri(&mut self, params: TooltipFromUriParams) -> Option<&mut Node> {
349        self.push_structure_child(
350            KindT::TooltipFromUri,
351            NodeParams::TooltipFromUriParams(params),
352        )
353    }
354
355    /// Add tooltips read from a category of the structure's own source file.
356    pub fn tooltip_from_source(&mut self, params: TooltipFromSourceParams) -> Option<&mut Node> {
357        self.push_structure_child(
358            KindT::TooltipFromSource,
359            NodeParams::TooltipFromSourceParams(params),
360        )
361    }
362
363    /// Append a child to a structure node; `None` if `self` is not a structure.
364    fn push_structure_child(&mut self, kind: KindT, params: NodeParams) -> Option<&mut Node> {
365        if self.kind != KindT::Structure {
366            return None;
367        }
368        let children = self.children.get_or_insert_with(Vec::new);
369        children.push(Node::new(kind, Some(params)));
370        children.last_mut()
371    }
372
373    /// Add a transform to a structure.
374    ///
375    /// Returns `None` if `self` is not a structure, or if `params.rotation` is
376    /// non-empty and not a 3x3 rotation matrix (see [`is_rotation_matrix`]); the
377    /// Python builder raises `ValueError` in that case.
378    pub fn transform(&mut self, params: TransformParams) -> Option<&mut Node> {
379        if let Some(rotation) = &params.rotation
380            && !rotation.is_empty()
381            && !is_rotation_matrix(rotation)
382        {
383            return None;
384        }
385        if self.kind == KindT::Structure {
386            let transform_node =
387                Node::new(KindT::Transform, Some(NodeParams::TransformParams(params)));
388            self.children
389                .get_or_insert_with(Vec::new)
390                .push(transform_node);
391            self.children.as_mut().unwrap().last_mut()
392        } else {
393            None
394        }
395    }
396    // Component methods ------------------------------------------------------
397
398    /// Add a representation for this component.
399    /// :param type: the type of representation, defaults to 'cartoon'
400    /// :return: a builder that handles operations at representation level
401    pub fn representation(
402        &mut self,
403        representation_type: RepresentationTypeT,
404    ) -> Option<&mut Node> {
405        self.representation_with_theme(representation_type, None)
406    }
407
408    pub fn representation_with_theme(
409        &mut self,
410        representation_type: RepresentationTypeT,
411        color_theme: Option<ColorThemeT>,
412    ) -> Option<&mut Node> {
413        if self.kind == KindT::Component {
414            let representation_node = Node::new(
415                KindT::Representation,
416                Some(NodeParams::RepresentationParams(RepresentationParams {
417                    representation_type,
418                    color_theme,
419                })),
420            );
421            self.children
422                .get_or_insert_with(Vec::new)
423                .push(representation_node);
424            self.children.as_mut().unwrap().last_mut()
425        } else {
426            None
427        }
428    }
429
430    pub fn label(&mut self, label: String) -> Option<&mut Node> {
431        if self.kind == KindT::Component {
432            let label_node = Node::new(
433                KindT::Label,
434                Some(NodeParams::LabelInlineParams(LabelInlineParams {
435                    text: label,
436                })),
437            );
438            self.children.get_or_insert_with(Vec::new).push(label_node);
439            self.children.as_mut().unwrap().last_mut()
440        } else {
441            None
442        }
443    }
444    pub fn tooltip(&mut self, text: String) -> Option<&mut Node> {
445        if self.kind == KindT::Component {
446            let node = Node::new(
447                KindT::Tooltip,
448                Some(NodeParams::TooltipInlineParams(TooltipInlineParams {
449                    text,
450                })),
451            );
452            self.children.get_or_insert_with(Vec::new).push(node);
453            self.children.as_mut().unwrap().last_mut()
454        } else {
455            None
456        }
457    }
458    pub fn focus(
459        &mut self,
460        direction: Option<(f64, f64, f64)>,
461        up: Option<(f64, f64, f64)>,
462    ) -> Option<&mut Node> {
463        if self.kind == KindT::Component {
464            let node = Node::new(
465                KindT::Focus,
466                Some(NodeParams::FocusInlineParams(FocusInlineParams {
467                    direction,
468                    up,
469                })),
470            );
471            self.children.get_or_insert_with(Vec::new).push(node);
472            self.children.as_mut().unwrap().last_mut()
473        } else {
474            None
475        }
476    }
477
478    // Representation methods ------------------------------------------------------
479
480    pub fn color_from_source(&mut self, params: ColorFromSourceParams) -> Option<&mut Node> {
481        if self.kind == KindT::Representation {
482            let node = Node::new(
483                KindT::ColorFromSource,
484                Some(NodeParams::ColorFromSourceParams(params)),
485            );
486            self.children.get_or_insert_with(Vec::new).push(node);
487            self.children.as_mut().unwrap().last_mut()
488        } else {
489            None
490        }
491    }
492    pub fn color_from_uri(&mut self, params: ColorFromUriParams) -> Option<&mut Node> {
493        if self.kind == KindT::Representation {
494            let node = Node::new(
495                KindT::ColorFromUri,
496                Some(NodeParams::ColorFromUriParams(params)),
497            );
498            self.children.get_or_insert_with(Vec::new).push(node);
499            self.children.as_mut().unwrap().last_mut()
500        } else {
501            None
502        }
503    }
504    // parent Kine => kindt:representation
505    // node: kindt => kindt:color
506    pub fn color(&mut self, color: ColorT, selector: ComponentSelector) -> Option<&mut Node> {
507        if self.kind == KindT::Representation {
508            let color_node = Node::new(
509                KindT::Color,
510                Some(NodeParams::ColorInlineParams(ColorInlineParams {
511                    base: ComponentInlineParams { selector },
512                    color,
513                })),
514            );
515            self.children.get_or_insert_with(Vec::new).push(color_node);
516            self.children.as_mut().unwrap().last_mut()
517        } else {
518            None
519        }
520    }
521
522    // GenericVisuals methods ------------------------------------------------------
523    pub fn sphere(&mut self, params: SphereParams) -> Option<&mut Node> {
524        if self.kind == KindT::GenericVisuals {
525            let node = Node::new(KindT::Sphere, Some(NodeParams::SphereParams(params)));
526            self.children.get_or_insert_with(Vec::new).push(node);
527            self.children.as_mut().unwrap().last_mut()
528        } else {
529            None
530        }
531    }
532    pub fn line(&mut self, params: LineParams) -> Option<&mut Node> {
533        if self.kind == KindT::GenericVisuals {
534            let node = Node::new(KindT::Line, Some(NodeParams::LineParams(params)));
535            self.children.get_or_insert_with(Vec::new).push(node);
536            self.children.as_mut().unwrap().last_mut()
537        } else {
538            None
539        }
540    }
541}
542
543impl<'de> Deserialize<'de> for Node {
544    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
545        #[derive(Deserialize)]
546        struct NodeHelper {
547            kind: KindT,
548            #[serde(default)]
549            params: Option<serde_json::Value>,
550            #[serde(default, deserialize_with = "deserialize_children")]
551            children: Option<Vec<Node>>,
552        }
553
554        let helper = NodeHelper::deserialize(deserializer)?;
555
556        let params = match helper.params {
557            // absent or explicit null → no params
558            None | Some(serde_json::Value::Null) => None,
559            Some(v) => {
560                let p: NodeParams = match &helper.kind {
561                    KindT::Download => {
562                        serde_json::from_value::<DownloadParams>(v).map(NodeParams::DownloadParams)
563                    }
564                    KindT::Parse => {
565                        serde_json::from_value::<ParseParams>(v).map(NodeParams::ParseParams)
566                    }
567                    KindT::Structure => serde_json::from_value::<StructureParams>(v)
568                        .map(NodeParams::StructureParams),
569                    KindT::Representation => serde_json::from_value::<RepresentationParams>(v)
570                        .map(NodeParams::RepresentationParams),
571                    KindT::Component => serde_json::from_value::<ComponentInlineParams>(v)
572                        .map(NodeParams::ComponentInlineParams),
573                    KindT::ComponentFromUri => serde_json::from_value::<ComponentFromUriParams>(v)
574                        .map(NodeParams::ComponentFromUriParams),
575                    KindT::ComponentFromSource => {
576                        serde_json::from_value::<ComponentFromSourceParams>(v)
577                            .map(NodeParams::ComponentFromSourceParams)
578                    }
579                    KindT::Color => serde_json::from_value::<ColorInlineParams>(v)
580                        .map(NodeParams::ColorInlineParams),
581                    KindT::ColorFromUri => serde_json::from_value::<ColorFromUriParams>(v)
582                        .map(NodeParams::ColorFromUriParams),
583                    KindT::ColorFromSource => serde_json::from_value::<ColorFromSourceParams>(v)
584                        .map(NodeParams::ColorFromSourceParams),
585                    KindT::Label => serde_json::from_value::<LabelInlineParams>(v)
586                        .map(NodeParams::LabelInlineParams),
587                    KindT::LabelFromUri => serde_json::from_value::<LabelFromUriParams>(v)
588                        .map(NodeParams::LabelFromUriParams),
589                    KindT::LabelFromSource => serde_json::from_value::<LabelFromSourceParams>(v)
590                        .map(NodeParams::LabelFromSourceParams),
591                    KindT::Tooltip => serde_json::from_value::<TooltipInlineParams>(v)
592                        .map(NodeParams::TooltipInlineParams),
593                    KindT::TooltipFromUri => serde_json::from_value::<TooltipFromUriParams>(v)
594                        .map(NodeParams::TooltipFromUriParams),
595                    KindT::TooltipFromSource => {
596                        serde_json::from_value::<TooltipFromSourceParams>(v)
597                            .map(NodeParams::TooltipFromSourceParams)
598                    }
599                    KindT::Focus => serde_json::from_value::<FocusInlineParams>(v)
600                        .map(NodeParams::FocusInlineParams),
601                    KindT::Transform => serde_json::from_value::<TransformParams>(v)
602                        .map(NodeParams::TransformParams),
603                    KindT::Camera => {
604                        serde_json::from_value::<CameraParams>(v).map(NodeParams::CameraParams)
605                    }
606                    KindT::Canvas => {
607                        serde_json::from_value::<CanvasParams>(v).map(NodeParams::CanvasParams)
608                    }
609                    KindT::Sphere => {
610                        serde_json::from_value::<SphereParams>(v).map(NodeParams::SphereParams)
611                    }
612                    KindT::Line => {
613                        serde_json::from_value::<LineParams>(v).map(NodeParams::LineParams)
614                    }
615                    KindT::Volume => {
616                        serde_json::from_value::<VolumeParams>(v).map(NodeParams::VolumeParams)
617                    }
618                    KindT::VolumeRepresentation => {
619                        serde_json::from_value::<VolumeRepresentationParams>(v)
620                            .map(NodeParams::VolumeRepresentationParams)
621                    }
622                    // Root and GenericVisuals carry no params; ignore any value present.
623                    KindT::Root | KindT::GenericVisuals => {
624                        return Ok(Node {
625                            kind: helper.kind,
626                            params: None,
627                            children: helper.children,
628                        });
629                    }
630                }
631                .map_err(|e| serde::de::Error::custom(e.to_string()))?;
632                Some(p)
633            }
634        };
635
636        Ok(Node {
637            kind: helper.kind,
638            params,
639            children: helper.children,
640        })
641    }
642}
643
644#[derive(Serialize, Deserialize, Debug, Clone)]
645#[serde(rename_all = "snake_case")]
646pub enum DescriptionFormatT {
647    Markdown,
648    Plaintext,
649}
650
651/// Metadata
652///
653/// The molviewspec metadata. High level info unrelated to
654/// structure visualization.
655///
656#[derive(Serialize, Deserialize, Debug, Default)]
657pub struct Metadata {
658    pub version: String,
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub title: Option<String>,
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub description: Option<String>,
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub description_format: Option<DescriptionFormatT>,
665    pub timestamp: String,
666}
667
668/// This is the base MolViewSpec object containing
669/// the root node and the metadata
670///
671/// Holds methods that modify the root node.
672///
673#[derive(Serialize, Deserialize, Debug)]
674pub struct State {
675    pub root: Node,
676    pub metadata: Metadata,
677}
678impl Default for State {
679    fn default() -> Self {
680        Self::new()
681    }
682}
683
684impl State {
685    pub fn new() -> Self {
686        State {
687            root: Node::new(KindT::Root, None),
688            metadata: Metadata {
689                version: "1.0".to_string(),
690                timestamp: Utc::now().to_rfc3339().to_string(),
691                ..Default::default()
692            },
693        }
694    }
695
696    /// Parse a MVSJ string into a State.
697    #[allow(clippy::should_implement_trait)] // fallible JSON parse, not std FromStr
698    pub fn from_str(s: &str) -> serde_json::Result<State> {
699        serde_json::from_str(s)
700    }
701
702    /// Parse a MVSJ reader into a State.
703    pub fn from_reader<R: Read>(reader: R) -> serde_json::Result<State> {
704        serde_json::from_reader(reader)
705    }
706
707    /// Set Camera Location
708    pub fn camera(&mut self, params: CameraParams) -> Option<&mut Node> {
709        if self.root.kind == KindT::Root {
710            let camera_node = Node::new(KindT::Camera, Some(NodeParams::CameraParams(params)));
711            self.root
712                .children
713                .get_or_insert_with(Vec::new)
714                .push(camera_node);
715            self.root.children.as_mut().unwrap().last_mut()
716        } else {
717            None
718        }
719    }
720
721    /// Set canvas background color.
722    pub fn canvas(&mut self, params: CanvasParams) -> Option<&mut Node> {
723        if self.root.kind == KindT::Root {
724            let node = Node::new(KindT::Canvas, Some(NodeParams::CanvasParams(params)));
725            self.root.children.get_or_insert_with(Vec::new).push(node);
726            self.root.children.as_mut().unwrap().last_mut()
727        } else {
728            None
729        }
730    }
731
732    // Download a file
733    pub fn download(&mut self, url: &str) -> Option<&mut Node> {
734        self.root.download(url)
735    }
736
737    /// Add a generic_visuals node for custom spheres and lines.
738    pub fn generic_visuals(&mut self) -> Option<&mut Node> {
739        if self.root.kind == KindT::Root {
740            let node = Node::new(KindT::GenericVisuals, None);
741            self.root.children.get_or_insert_with(Vec::new).push(node);
742            self.root.children.as_mut().unwrap().last_mut()
743        } else {
744            None
745        }
746    }
747
748    /// Consume this `State` as the sole snapshot of an `MvsjFile`.
749    pub fn into_snapshot(self) -> Snapshot {
750        Snapshot {
751            root: self.root,
752            title: None,
753            description: None,
754            description_format: None,
755            key: None,
756            linger_duration_ms: None,
757            transition_duration_ms: None,
758        }
759    }
760
761    pub fn to_url(&self) -> String {
762        let json = serde_json::to_string(&self).expect("Json conversion");
763        let encoded = urlencoding::encode(&json);
764        format!(
765            "https://molstar.org/viewer/?mvs-format=mvsj&mvs-data={}",
766            encoded
767        )
768    }
769}
770
771/// A single scene state within a `MvsjFile` snapshot sequence.
772///
773/// A sequence of snapshots defines a guided tour: the viewer renders `root`, waits
774/// `linger_duration_ms`, then animates to the next snapshot's `root` over
775/// `transition_duration_ms`. `title`/`description`/`key` are UI metadata (e.g. table
776/// of contents entries), not rendering inputs.
777#[derive(Serialize, Deserialize, Debug, Clone)]
778pub struct Snapshot {
779    pub root: Node,
780    #[serde(skip_serializing_if = "Option::is_none")]
781    pub title: Option<String>,
782    #[serde(skip_serializing_if = "Option::is_none")]
783    pub description: Option<String>,
784    #[serde(skip_serializing_if = "Option::is_none")]
785    pub description_format: Option<DescriptionFormatT>,
786    #[serde(skip_serializing_if = "Option::is_none")]
787    pub key: Option<String>,
788    #[serde(skip_serializing_if = "Option::is_none")]
789    pub linger_duration_ms: Option<u64>,
790    #[serde(skip_serializing_if = "Option::is_none")]
791    pub transition_duration_ms: Option<u64>,
792}
793
794/// A multi-snapshot `.mvsj` document (a "story"): an ordered sequence of scene
795/// states sharing one metadata block, as produced by tools like mol-view-stories.
796///
797/// This is distinct from `State`, which holds a single root node. Use `State` for
798/// single-frame MVSJ files and `MvsjFile` for animated/guided-tour sequences.
799#[derive(Serialize, Deserialize, Debug)]
800pub struct MvsjFile {
801    pub metadata: Metadata,
802    pub snapshots: Vec<Snapshot>,
803}
804impl MvsjFile {
805    /// Parse a multi-snapshot MVSJ string into an `MvsjFile`.
806    #[allow(clippy::should_implement_trait)] // fallible JSON parse, not std FromStr
807    pub fn from_str(s: &str) -> serde_json::Result<MvsjFile> {
808        serde_json::from_str(s)
809    }
810
811    /// Parse a multi-snapshot MVSJ reader into an `MvsjFile`.
812    pub fn from_reader<R: Read>(reader: R) -> serde_json::Result<MvsjFile> {
813        serde_json::from_reader(reader)
814    }
815}
816
817/// Types of compounds: for pse I am only using PDB
818#[derive(Serialize, Deserialize, Debug, Clone)]
819#[serde(rename_all = "lowercase")]
820pub enum ParseFormatT {
821    Mmcif,
822    Bcif,
823    Pdb,
824}
825
826#[derive(Serialize, Deserialize, Debug, Clone)]
827#[serde(rename = "parse")]
828pub struct ParseParams {
829    pub format: ParseFormatT,
830}
831
832/// StructureType. Useful for specifying more complicated sets of structures
833#[derive(Serialize, Deserialize, Debug, Clone)]
834#[serde(rename_all = "snake_case")]
835#[derive(Default)]
836pub enum StructureTypeT {
837    Model,
838    #[default]
839    Assembly,
840    Symmetry,
841    SymmetryMates,
842}
843
844/// Structure Params
845#[derive(Serialize, Deserialize, Debug, Clone, Default)]
846pub struct StructureParams {
847    #[serde(rename = "type")]
848    pub structure_type: StructureTypeT,
849    #[serde(skip_serializing_if = "Option::is_none")]
850    pub assembly_id: Option<String>,
851    #[serde(skip_serializing_if = "Option::is_none")]
852    pub assembly_index: Option<i32>,
853    #[serde(skip_serializing_if = "Option::is_none")]
854    pub model_index: Option<i32>,
855    #[serde(skip_serializing_if = "Option::is_none")]
856    pub block_index: Option<i32>,
857    #[serde(skip_serializing_if = "Option::is_none")]
858    pub block_header: Option<String>,
859    #[serde(skip_serializing_if = "Option::is_none")]
860    pub radius: Option<f64>,
861    #[serde(skip_serializing_if = "Option::is_none")]
862    pub ijk_min: Option<(i32, i32, i32)>,
863    #[serde(skip_serializing_if = "Option::is_none")]
864    pub ijk_max: Option<(i32, i32, i32)>,
865}
866
867/// Component Selector Type
868///
869/// Useful for specifying broad groups like 'all',
870/// 'protein', etc.
871#[derive(Serialize, Deserialize, Debug, Clone)]
872#[serde(rename_all = "snake_case")]
873pub enum ComponentSelectorT {
874    All,
875    Polymer,
876    Protein,
877    Nucleic,
878    Branched,
879    Ligand,
880    Ion,
881    Water,
882}
883/// Component Expresssion
884#[derive(Serialize, Deserialize, Debug, Clone, Default)]
885pub struct ComponentExpression {
886    #[serde(skip_serializing_if = "Option::is_none")]
887    pub label_entity_id: Option<String>,
888    #[serde(skip_serializing_if = "Option::is_none")]
889    pub label_asym_id: Option<String>,
890    #[serde(skip_serializing_if = "Option::is_none")]
891    pub auth_asym_id: Option<String>,
892    #[serde(skip_serializing_if = "Option::is_none")]
893    pub label_seq_id: Option<i32>,
894    #[serde(skip_serializing_if = "Option::is_none")]
895    pub auth_seq_id: Option<i32>,
896    #[serde(skip_serializing_if = "Option::is_none")]
897    pub pdbx_pdb_ins_code: Option<String>,
898    #[serde(skip_serializing_if = "Option::is_none")]
899    pub beg_label_seq_id: Option<i32>,
900    #[serde(skip_serializing_if = "Option::is_none")]
901    pub end_label_seq_id: Option<i32>,
902    #[serde(skip_serializing_if = "Option::is_none")]
903    pub beg_auth_seq_id: Option<i32>,
904    #[serde(skip_serializing_if = "Option::is_none")]
905    pub end_auth_seq_id: Option<i32>,
906    #[serde(skip_serializing_if = "Option::is_none")]
907    pub residue_index: Option<i32>,
908    #[serde(skip_serializing_if = "Option::is_none")]
909    pub label_atom_id: Option<String>,
910    #[serde(skip_serializing_if = "Option::is_none")]
911    pub auth_atom_id: Option<String>,
912    #[serde(skip_serializing_if = "Option::is_none")]
913    pub type_symbol: Option<String>,
914    #[serde(skip_serializing_if = "Option::is_none")]
915    pub atom_id: Option<i32>,
916    #[serde(skip_serializing_if = "Option::is_none")]
917    pub atom_index: Option<i32>,
918}
919
920/// Representation Type
921#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
922#[serde(rename_all = "snake_case")]
923pub enum RepresentationTypeT {
924    BallAndStick,
925    Cartoon,
926    /// CA backbone line trace — lightweight, useful for large structures.
927    Line,
928    /// Variable-radius backbone tube; tube radius encodes B-factor/flexibility.
929    Putty,
930    /// Van der Waals spheres — shows full atomic volume.
931    Spacefill,
932    Surface,
933}
934
935/// Color theme applied to a representation as a whole.
936///
937/// Semantic themes assign per-atom colors based on structural or chemical
938/// properties rather than a single inline color.
939#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
940#[serde(rename_all = "snake_case")]
941pub enum ColorThemeT {
942    /// Each atom colored by CPK element convention (C=gray, N=blue, O=red, S=yellow, …).
943    #[default]
944    ElementSymbol,
945    /// Each chain gets a distinct color drawn from a rotating palette.
946    ChainId,
947    /// Helix=red/salmon, strand=yellow/gold, coil=white/gray.
948    SecondaryStructure,
949    /// All atoms share a single uniform white color (useful as a base for Color child overrides).
950    Uniform,
951    /// Maps a per-atom B-factor (crystallography) or pLDDT (AlphaFold) value to a
952    /// blue (low) → white → red (high) gradient over the observed value range.
953    Uncertainty,
954    /// AlphaFold-style pLDDT confidence banding: very high (>90, blue), confident
955    /// (70-90, teal), low (50-70, yellow), very low (<50, orange).
956    PlddtConfidence,
957    /// Maps per-atom occupancy (0.0-1.0) to a gradient, useful for spotting
958    /// alternate-conformer atoms with partial occupancy.
959    Occupancy,
960}
961
962/// Color Names
963#[derive(Serialize, Deserialize, Debug, Clone)]
964#[serde(rename_all = "lowercase")]
965pub enum ColorNamesT {
966    Aliceblue,
967    Antiquewhite,
968    Aqua,
969    Aquamarine,
970    Azure,
971    Beige,
972    Bisque,
973    Black,
974    Blanchedalmond,
975    Blue,
976    Blueviolet,
977    Brown,
978    Burlywood,
979    Cadetblue,
980    Chartreuse,
981    Chocolate,
982    Coral,
983    Cornflowerblue,
984    Cornsilk,
985    Crimson,
986    Cyan,
987    Darkblue,
988    Darkcyan,
989    Darkgoldenrod,
990    Darkgray,
991    Darkgreen,
992    Darkgrey,
993    Darkkhaki,
994    Darkmagenta,
995    Darkolivegreen,
996    Darkorange,
997    Darkorchid,
998    Darkred,
999    Darksalmon,
1000    Darkseagreen,
1001    Darkslateblue,
1002    Darkslategray,
1003    Darkslategrey,
1004    Darkturquoise,
1005    Darkviolet,
1006    Deeppink,
1007    Deepskyblue,
1008    Dimgray,
1009    Dimgrey,
1010    Dodgerblue,
1011    Firebrick,
1012    Floralwhite,
1013    Forestgreen,
1014    Fuchsia,
1015    Gainsboro,
1016    Ghostwhite,
1017    Gold,
1018    Goldenrod,
1019    Gray,
1020    Green,
1021    Greenyellow,
1022    Grey,
1023    Honeydew,
1024    Hotpink,
1025    Indianred,
1026    Indigo,
1027    Ivory,
1028    Khaki,
1029    Lavender,
1030    Lavenderblush,
1031    Lawngreen,
1032    Lemonchiffon,
1033    Lightblue,
1034    Lightcoral,
1035    Lightcyan,
1036    Lightgoldenrodyellow,
1037    Lightgray,
1038    Lightgreen,
1039    Lightgrey,
1040    Lightpink,
1041    Lightsalmon,
1042    Lightseagreen,
1043    Lightskyblue,
1044    Lightslategray,
1045    Lightslategrey,
1046    Lightsteelblue,
1047    Lightyellow,
1048    Lime,
1049    Limegreen,
1050    Linen,
1051    Magenta,
1052    Maroon,
1053    Mediumaquamarine,
1054    Mediumblue,
1055    Mediumorchid,
1056    Mediumpurple,
1057    Mediumseagreen,
1058    Mediumslateblue,
1059    Mediumspringgreen,
1060    Mediumturquoise,
1061    Mediumvioletred,
1062    Midnightblue,
1063    Mintcream,
1064    Mistyrose,
1065    Moccasin,
1066    Navajowhite,
1067    Navy,
1068    Oldlace,
1069    Olive,
1070    Olivedrab,
1071    Orange,
1072    Orangered,
1073    Orchid,
1074    Palegoldenrod,
1075    Palegreen,
1076    Paleturquoise,
1077    Palevioletred,
1078    Papayawhip,
1079    Peachpuff,
1080    Peru,
1081    Pink,
1082    Plum,
1083    Powderblue,
1084    Purple,
1085    Red,
1086    Rosybrown,
1087    Royalblue,
1088    Saddlebrown,
1089    Salmon,
1090    Sandybrown,
1091    Seagreen,
1092    Seashell,
1093    Sienna,
1094    Silver,
1095    Skyblue,
1096    Slateblue,
1097    Slategray,
1098    Slategrey,
1099    Snow,
1100    Springgreen,
1101    Steelblue,
1102    Tan,
1103    Teal,
1104    Thistle,
1105    Tomato,
1106    Turquoise,
1107    Violet,
1108    Wheat,
1109    White,
1110    Whitesmoke,
1111    Yellow,
1112    Yellowgreen,
1113}
1114
1115/// Color Type: Named or Hex
1116#[derive(Serialize, Deserialize, Debug, Clone)]
1117#[serde(untagged)]
1118pub enum ColorT {
1119    Named(ColorNamesT),
1120    Hex(String),
1121}
1122
1123#[derive(Serialize, Deserialize, Debug, Clone)]
1124pub struct RepresentationParams {
1125    #[serde(rename = "type")]
1126    pub representation_type: RepresentationTypeT,
1127    /// Semantic color theme applied before any inline `Color` child overrides.
1128    /// Defaults to `ElementSymbol` (CPK coloring) when absent.
1129    #[serde(skip_serializing_if = "Option::is_none")]
1130    pub color_theme: Option<ColorThemeT>,
1131}
1132
1133/// Params for a `Volume` node — parallel to `StructureParams`, references volumetric
1134/// data (e.g. a CCP4/MRC/DSN6 electron-density or EM map) parsed from a `Download`+`Parse` pair.
1135#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1136pub struct VolumeParams {
1137    #[serde(skip_serializing_if = "Option::is_none")]
1138    pub channel_id: Option<String>,
1139}
1140
1141/// Type of volume representation: a 3D isosurface or a 2D cross-sectional slice.
1142#[derive(Serialize, Deserialize, Debug, Clone)]
1143#[serde(rename_all = "snake_case")]
1144pub enum VolumeRepresentationTypeT {
1145    Isosurface,
1146    GridSlice,
1147}
1148
1149/// Params for an isosurface volume representation.
1150///
1151/// `isovalue` is an absolute data-unit threshold; `relative_isovalue` is expressed in
1152/// standard deviations above the mean and is the more common way to specify a contour
1153/// level since raw absolute values vary widely between maps.
1154#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1155pub struct IsoSurfaceParams {
1156    #[serde(skip_serializing_if = "Option::is_none")]
1157    pub isovalue: Option<f64>,
1158    #[serde(skip_serializing_if = "Option::is_none")]
1159    pub relative_isovalue: Option<f32>,
1160    #[serde(skip_serializing_if = "Option::is_none")]
1161    pub wireframe: Option<bool>,
1162    #[serde(skip_serializing_if = "Option::is_none")]
1163    pub opacity: Option<f32>,
1164}
1165
1166#[derive(Serialize, Deserialize, Debug, Clone)]
1167pub struct VolumeRepresentationParams {
1168    #[serde(rename = "type")]
1169    pub representation_type: VolumeRepresentationTypeT,
1170    #[serde(flatten)]
1171    pub isosurface: Option<IsoSurfaceParams>,
1172}
1173
1174#[derive(Serialize, Deserialize, Debug, Clone)]
1175#[serde(rename_all = "snake_case")]
1176pub enum SchemaT {
1177    WholeStructure,
1178    Entity,
1179    Chain,
1180    AuthChain,
1181    Residue,
1182    AuthResidue,
1183    ResidueRange,
1184    AuthResidueRange,
1185    Atom,
1186    AuthAtom,
1187    AllAtomic,
1188}
1189
1190#[derive(Serialize, Deserialize, Debug, Clone)]
1191#[serde(rename_all = "lowercase")]
1192pub enum SchemaFormatT {
1193    Cif,
1194    Bcif,
1195    Json,
1196}
1197
1198#[derive(Serialize, Deserialize, Debug, Clone)]
1199pub struct DataFromUriParams {
1200    pub uri: String,
1201    pub format: SchemaFormatT,
1202    #[serde(skip_serializing_if = "Option::is_none")]
1203    pub category_name: Option<String>,
1204    #[serde(skip_serializing_if = "Option::is_none")]
1205    pub field_name: Option<String>,
1206    #[serde(skip_serializing_if = "Option::is_none")]
1207    pub block_header: Option<String>,
1208    #[serde(skip_serializing_if = "Option::is_none")]
1209    pub block_index: Option<i32>,
1210    #[serde(rename = "schema")]
1211    pub schema_: SchemaT,
1212}
1213
1214#[derive(Serialize, Deserialize, Debug, Clone)]
1215pub struct DataFromSourceParams {
1216    pub category_name: String,
1217    #[serde(skip_serializing_if = "Option::is_none")]
1218    pub field_name: Option<String>,
1219    #[serde(skip_serializing_if = "Option::is_none")]
1220    pub block_header: Option<String>,
1221    #[serde(skip_serializing_if = "Option::is_none")]
1222    pub block_index: Option<i32>,
1223    #[serde(rename = "schema")]
1224    pub schema_: SchemaT,
1225}
1226
1227#[derive(Serialize, Deserialize, Debug, Clone)]
1228pub struct ComponentInlineParams {
1229    pub selector: ComponentSelector,
1230}
1231
1232#[derive(Serialize, Deserialize, Debug, Clone)]
1233#[serde(untagged)]
1234#[allow(clippy::large_enum_variant)] // untagged serde enum; boxing would change the wire shape
1235pub enum ComponentSelector {
1236    Selector(ComponentSelectorT),
1237    Expression(ComponentExpression),
1238    ExpressionList(Vec<ComponentExpression>),
1239}
1240impl Default for ComponentSelector {
1241    fn default() -> Self {
1242        ComponentSelector::Selector(ComponentSelectorT::All)
1243    }
1244}
1245
1246#[derive(Serialize, Deserialize, Debug, Clone)]
1247pub struct ComponentFromUriParams {
1248    #[serde(flatten)]
1249    pub base: DataFromUriParams,
1250    #[serde(skip_serializing_if = "Option::is_none")]
1251    pub field_values: Option<Vec<String>>,
1252}
1253
1254#[derive(Serialize, Deserialize, Debug, Clone)]
1255pub struct ComponentFromSourceParams {
1256    #[serde(flatten)]
1257    pub base: DataFromSourceParams,
1258    #[serde(skip_serializing_if = "Option::is_none")]
1259    pub field_values: Option<Vec<String>>,
1260}
1261
1262#[derive(Serialize, Deserialize, Debug, Clone)]
1263pub struct ColorInlineParams {
1264    #[serde(flatten)]
1265    pub base: ComponentInlineParams,
1266    pub color: ColorT,
1267}
1268
1269#[derive(Serialize, Deserialize, Debug, Clone)]
1270pub struct ColorFromUriParams {
1271    #[serde(flatten)]
1272    pub base: DataFromUriParams,
1273}
1274
1275#[derive(Serialize, Deserialize, Debug, Clone)]
1276pub struct ColorFromSourceParams {
1277    #[serde(flatten)]
1278    pub base: DataFromSourceParams,
1279}
1280
1281#[derive(Serialize, Deserialize, Debug, Clone)]
1282pub struct LabelInlineParams {
1283    pub text: String,
1284}
1285
1286#[derive(Serialize, Deserialize, Debug, Clone)]
1287pub struct LabelFromUriParams {
1288    #[serde(flatten)]
1289    pub base: DataFromUriParams,
1290}
1291
1292#[derive(Serialize, Deserialize, Debug, Clone)]
1293pub struct LabelFromSourceParams {
1294    #[serde(flatten)]
1295    pub base: DataFromSourceParams,
1296}
1297
1298#[derive(Serialize, Deserialize, Debug, Clone)]
1299pub struct TooltipInlineParams {
1300    pub text: String,
1301}
1302
1303#[derive(Serialize, Deserialize, Debug, Clone)]
1304pub struct TooltipFromUriParams {
1305    #[serde(flatten)]
1306    pub base: DataFromUriParams,
1307}
1308
1309#[derive(Serialize, Deserialize, Debug, Clone)]
1310pub struct TooltipFromSourceParams {
1311    #[serde(flatten)]
1312    pub base: DataFromSourceParams,
1313}
1314
1315#[derive(Serialize, Deserialize, Debug, Clone)]
1316pub struct FocusInlineParams {
1317    #[serde(skip_serializing_if = "Option::is_none")]
1318    pub direction: Option<(f64, f64, f64)>,
1319    #[serde(skip_serializing_if = "Option::is_none")]
1320    pub up: Option<(f64, f64, f64)>,
1321}
1322
1323#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1324pub struct TransformParams {
1325    #[serde(skip_serializing_if = "Option::is_none")]
1326    pub rotation: Option<Vec<f64>>,
1327    #[serde(skip_serializing_if = "Option::is_none")]
1328    pub translation: Option<(f64, f64, f64)>,
1329}
1330
1331#[derive(Serialize, Deserialize, Debug, Clone)]
1332pub struct SphereParams {
1333    pub position: (f64, f64, f64),
1334    pub radius: f64,
1335    pub color: ColorT,
1336    #[serde(skip_serializing_if = "Option::is_none")]
1337    pub label: Option<String>,
1338    #[serde(skip_serializing_if = "Option::is_none")]
1339    pub tooltip: Option<String>,
1340}
1341
1342#[derive(Serialize, Deserialize, Debug, Clone)]
1343pub struct LineParams {
1344    pub position1: (f64, f64, f64),
1345    pub position2: (f64, f64, f64),
1346    pub radius: f64,
1347    pub color: ColorT,
1348    #[serde(skip_serializing_if = "Option::is_none")]
1349    pub label: Option<String>,
1350    #[serde(skip_serializing_if = "Option::is_none")]
1351    pub tooltip: Option<String>,
1352}
1353
1354#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1355pub struct CameraParams {
1356    pub target: (f64, f64, f64),
1357    pub position: (f64, f64, f64),
1358    #[serde(skip_serializing_if = "Option::is_none")]
1359    pub up: Option<(f64, f64, f64)>,
1360}
1361
1362#[derive(Serialize, Deserialize, Debug, Clone)]
1363pub struct CanvasParams {
1364    pub background_color: ColorT,
1365}
1366
1367#[derive(Debug, Serialize, Deserialize, Validate, Clone)]
1368pub struct DownloadParams {
1369    pub url: String,
1370}