1use serde::{Deserialize, Serialize};
18
19#[cfg(feature = "codegen")]
20use schemars::JsonSchema;
21#[cfg(feature = "codegen")]
22use ts_rs::TS;
23
24use crate::edge::AlignOpts;
25
26#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
32#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
33#[cfg_attr(feature = "codegen", ts(export))]
34pub struct ShadeStyle {
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 #[cfg_attr(feature = "codegen", ts(optional))]
37 pub colormap: Option<String>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 #[cfg_attr(feature = "codegen", ts(optional))]
40 pub vmin: Option<f64>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 #[cfg_attr(feature = "codegen", ts(optional))]
43 pub vmax: Option<f64>,
44}
45
46impl ShadeStyle {
47 pub fn merge_from(&mut self, other: &ShadeStyle) {
48 if other.colormap.is_some() {
49 self.colormap = other.colormap.clone();
50 }
51 if other.vmin.is_some() {
52 self.vmin = other.vmin;
53 }
54 if other.vmax.is_some() {
55 self.vmax = other.vmax;
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
65#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
66#[cfg_attr(feature = "codegen", ts(export))]
67pub struct ShadeSpec {
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 #[cfg_attr(feature = "codegen", ts(optional))]
70 pub atoms: Option<Vec<f64>>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 #[cfg_attr(feature = "codegen", ts(optional))]
73 pub bonds: Option<Vec<f64>>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 #[cfg_attr(feature = "codegen", ts(optional))]
76 pub colormap: Option<String>,
77 #[serde(default = "default_shade_vmin")]
78 pub vmin: f64,
79 #[serde(default = "default_shade_vmax")]
80 pub vmax: f64,
81}
82
83fn default_shade_vmin() -> f64 {
84 0.0
85}
86fn default_shade_vmax() -> f64 {
87 1.0
88}
89
90impl Default for ShadeSpec {
91 fn default() -> Self {
92 Self {
93 atoms: None,
94 bonds: None,
95 colormap: None,
96 vmin: 0.0,
97 vmax: 1.0,
98 }
99 }
100}
101
102impl ShadeSpec {
103 pub fn style(&self) -> ShadeStyle {
105 ShadeStyle {
106 colormap: self.colormap.clone(),
107 vmin: Some(self.vmin),
108 vmax: Some(self.vmax),
109 }
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
120#[cfg_attr(feature = "codegen", ts(export))]
121pub struct AlignToSpec {
122 #[serde(rename = "ref")]
124 #[cfg_attr(feature = "codegen", ts(rename = "ref"))]
125 pub ref_id: String,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 #[cfg_attr(feature = "codegen", ts(optional))]
129 pub atom_map: Option<Vec<(u32, u32)>>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 #[cfg_attr(feature = "codegen", ts(optional))]
133 pub min_atoms: Option<u32>,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[serde(untagged)]
139#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
140#[cfg_attr(feature = "codegen", ts(export))]
141pub enum AlignTo {
142 Ref(String),
144 Spec(AlignToSpec),
145}
146
147impl AlignTo {
148 pub fn ref_id(&self) -> &str {
149 match self {
150 AlignTo::Ref(s) => s.as_str(),
151 AlignTo::Spec(s) => s.ref_id.as_str(),
152 }
153 }
154
155 pub fn align_opts(&self) -> AlignOpts {
156 match self {
157 AlignTo::Ref(_) => AlignOpts::default(),
158 AlignTo::Spec(s) => AlignOpts {
159 atom_map: s.atom_map.clone(),
160 min_atoms: s.min_atoms,
161 },
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
172#[serde(rename_all = "snake_case")]
173#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
174#[cfg_attr(feature = "codegen", ts(export))]
175pub enum NodeType {
176 Mol,
177 Group,
178 ReactionScheme,
180 Edge,
182 Text,
184}
185
186impl NodeType {
187 pub fn as_str(self) -> &'static str {
188 match self {
189 NodeType::Mol => "mol",
190 NodeType::Group => "group",
191 NodeType::ReactionScheme => "reaction_scheme",
192 NodeType::Edge => "edge",
193 NodeType::Text => "text",
194 }
195 }
196}
197
198#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
200#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
201#[cfg_attr(feature = "codegen", ts(export))]
202pub struct CommonOpts {
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 #[cfg_attr(feature = "codegen", ts(optional))]
205 pub color: Option<String>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 #[cfg_attr(feature = "codegen", ts(optional))]
208 pub scale: Option<f64>,
209}
210
211impl CommonOpts {
212 pub fn merge_from(&mut self, other: &CommonOpts) {
213 if other.color.is_some() {
214 self.color = other.color.clone();
215 }
216 if other.scale.is_some() {
217 self.scale = other.scale;
218 }
219 }
220}
221
222#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
227#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
228#[cfg_attr(feature = "codegen", ts(export))]
229pub struct MolOpts {
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 #[cfg_attr(feature = "codegen", ts(optional))]
232 pub color: Option<String>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 #[cfg_attr(feature = "codegen", ts(optional))]
235 pub scale: Option<f64>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 #[cfg_attr(feature = "codegen", ts(optional))]
238 pub weight: Option<f64>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 #[cfg_attr(feature = "codegen", ts(optional))]
241 pub halo: Option<bool>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 #[cfg_attr(feature = "codegen", ts(optional))]
244 pub shade: Option<ShadeStyle>,
245}
246
247impl MolOpts {
248 pub fn merge_from(&mut self, other: &MolOpts) {
249 if other.color.is_some() {
250 self.color = other.color.clone();
251 }
252 if other.scale.is_some() {
253 self.scale = other.scale;
254 }
255 if other.weight.is_some() {
256 self.weight = other.weight;
257 }
258 if other.halo.is_some() {
259 self.halo = other.halo;
260 }
261 if let Some(ref s) = other.shade {
262 self.shade.get_or_insert_with(ShadeStyle::default).merge_from(s);
263 }
264 }
265
266 pub fn merge_common(&mut self, common: &CommonOpts) {
267 if common.color.is_some() {
268 self.color = common.color.clone();
269 }
270 if common.scale.is_some() {
271 self.scale = common.scale;
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
278#[serde(tag = "type", rename_all = "snake_case")]
279#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
280#[cfg_attr(feature = "codegen", ts(export))]
281pub enum TypedOptsPatch {
282 Mol {
283 #[serde(flatten)]
284 opts: MolOpts,
285 },
286 Group {
287 #[serde(flatten)]
288 opts: CommonOpts,
289 },
290 ReactionScheme {
291 #[serde(flatten)]
292 opts: CommonOpts,
293 },
294 Edge {
295 #[serde(flatten)]
296 opts: CommonOpts,
297 },
298 Text {
299 #[serde(flatten)]
300 opts: CommonOpts,
301 },
302}
303
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
309#[cfg_attr(feature = "codegen", ts(export))]
310pub struct ForTypesPatch {
311 pub for_types: Vec<NodeType>,
312 #[serde(flatten)]
313 pub opts: CommonOpts,
314}
315
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
318#[serde(untagged)]
319#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
320#[cfg_attr(feature = "codegen", ts(export))]
321pub enum OptsPatch {
322 Typed(TypedOptsPatch),
324 ForTypes(ForTypesPatch),
326 Universal(CommonOpts),
328}
329
330impl OptsPatch {
331 pub fn applies_to(&self, target: NodeType) -> bool {
333 match self {
334 OptsPatch::Typed(TypedOptsPatch::Mol { .. }) => target == NodeType::Mol,
335 OptsPatch::Typed(TypedOptsPatch::Group { .. }) => target == NodeType::Group,
336 OptsPatch::Typed(TypedOptsPatch::ReactionScheme { .. }) => {
337 target == NodeType::ReactionScheme
338 }
339 OptsPatch::Typed(TypedOptsPatch::Edge { .. }) => target == NodeType::Edge,
340 OptsPatch::Typed(TypedOptsPatch::Text { .. }) => target == NodeType::Text,
341 OptsPatch::ForTypes(p) => p.for_types.contains(&target),
342 OptsPatch::Universal(_) => true,
343 }
344 }
345
346 pub fn apply_to_mol(&self, out: &mut MolOpts) {
347 match self {
348 OptsPatch::Typed(TypedOptsPatch::Mol { opts }) => out.merge_from(opts),
349 OptsPatch::Typed(TypedOptsPatch::Group { .. }) => {}
350 OptsPatch::Typed(TypedOptsPatch::ReactionScheme { .. }) => {}
351 OptsPatch::Typed(TypedOptsPatch::Edge { .. }) => {}
352 OptsPatch::Typed(TypedOptsPatch::Text { .. }) => {}
353 OptsPatch::ForTypes(p) => out.merge_common(&p.opts),
354 OptsPatch::Universal(c) => out.merge_common(c),
355 }
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
361#[serde(untagged)]
362#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
363#[cfg_attr(feature = "codegen", ts(export))]
364pub enum Opts {
365 One(OptsPatch),
366 Many(Vec<OptsPatch>),
367}
368
369impl Opts {
370 pub fn patches(&self) -> Vec<&OptsPatch> {
371 match self {
372 Opts::One(p) => vec![p],
373 Opts::Many(ps) => ps.iter().collect(),
374 }
375 }
376
377 pub fn apply_to_mol(&self, out: &mut MolOpts) {
378 for p in self.patches() {
379 if p.applies_to(NodeType::Mol) {
380 p.apply_to_mol(out);
381 }
382 }
383 }
384}
385
386pub fn resolve_mol_opts<'a, I>(bags: I) -> MolOpts
388where
389 I: IntoIterator<Item = Option<&'a Opts>>,
390{
391 let mut out = MolOpts::default();
392 for bag in bags {
393 if let Some(opts) = bag {
394 opts.apply_to_mol(&mut out);
395 }
396 }
397 out
398}
399
400#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
406#[serde(rename_all = "lowercase")]
407#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
408#[cfg_attr(feature = "codegen", ts(export))]
409pub enum MolNodeKind {
410 #[default]
411 Mol,
412}
413
414#[derive(Debug, Clone, Default, Serialize, Deserialize)]
416#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
417#[cfg_attr(feature = "codegen", ts(export))]
418pub struct MolNode {
419 #[serde(rename = "type", default)]
420 pub type_: MolNodeKind,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
423 #[cfg_attr(feature = "codegen", ts(optional))]
424 pub smiles: Option<String>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
426 #[cfg_attr(feature = "codegen", ts(optional))]
427 pub cxsmiles: Option<String>,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 #[cfg_attr(feature = "codegen", ts(optional))]
430 pub molfile: Option<String>,
431 #[serde(default, skip_serializing_if = "Option::is_none")]
432 #[cfg_attr(feature = "codegen", ts(optional))]
433 pub id: Option<String>,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
436 #[cfg_attr(feature = "codegen", ts(optional))]
437 pub shade: Option<ShadeSpec>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 #[cfg_attr(feature = "codegen", ts(optional))]
440 pub star_labels: Option<Vec<Option<String>>>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
443 #[cfg_attr(feature = "codegen", ts(optional))]
444 pub align_to: Option<AlignTo>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
450 #[cfg_attr(feature = "codegen", ts(optional))]
451 pub label: Option<Label>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
454 #[cfg_attr(feature = "codegen", ts(optional))]
455 pub color: Option<String>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
457 #[cfg_attr(feature = "codegen", ts(optional))]
458 pub scale: Option<f64>,
459 #[serde(default, skip_serializing_if = "Option::is_none")]
460 #[cfg_attr(feature = "codegen", ts(optional))]
461 pub weight: Option<f64>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
463 #[cfg_attr(feature = "codegen", ts(optional))]
464 pub halo: Option<bool>,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
467 #[cfg_attr(feature = "codegen", ts(optional))]
468 pub opts: Option<Opts>,
469}
470
471impl MolNode {
472 pub fn local_opts(&self) -> MolOpts {
474 let mut o = MolOpts {
475 color: self.color.clone(),
476 scale: self.scale,
477 weight: self.weight,
478 halo: self.halo,
479 shade: None,
480 };
481 if let Some(ref shade) = self.shade {
482 o.shade = Some(shade.style());
483 }
484 o
485 }
486
487 pub fn structure(&self) -> Result<&str, String> {
488 for s in [&self.molfile, &self.cxsmiles, &self.smiles] {
489 if let Some(t) = s.as_ref().filter(|x| !x.trim().is_empty()) {
490 return Ok(t.as_str());
491 }
492 }
493 Err("mol node needs smiles, cxsmiles, or molfile".into())
494 }
495}
496
497#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
503#[serde(rename_all = "snake_case")]
504#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
505#[cfg_attr(feature = "codegen", ts(export))]
506pub enum EdgeArrow {
507 #[default]
509 Forward,
510 Equilibrium,
512 Open,
514 Line,
516}
517
518#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
520#[serde(rename_all = "snake_case")]
521#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
522#[cfg_attr(feature = "codegen", ts(export))]
523pub enum EdgeNodeKind {
524 #[default]
525 Edge,
526}
527
528#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
530#[serde(rename_all = "snake_case")]
531#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
532#[cfg_attr(feature = "codegen", ts(export))]
533pub enum TextNodeKind {
534 #[default]
535 Text,
536}
537
538#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
544#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
545#[cfg_attr(feature = "codegen", ts(export))]
546pub struct TextNode {
547 #[serde(rename = "type")]
548 #[cfg_attr(feature = "codegen", ts(rename = "type"))]
549 pub type_: TextNodeKind,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
552 #[cfg_attr(feature = "codegen", ts(optional))]
553 pub id: Option<String>,
554 pub text: String,
556 #[serde(default, skip_serializing_if = "Option::is_none")]
557 #[cfg_attr(feature = "codegen", ts(optional))]
558 pub color: Option<String>,
559 #[serde(default, skip_serializing_if = "Option::is_none")]
560 #[cfg_attr(feature = "codegen", ts(optional))]
561 pub scale: Option<f64>,
562 #[serde(default, skip_serializing_if = "Option::is_none")]
563 #[cfg_attr(feature = "codegen", ts(optional))]
564 pub opts: Option<Opts>,
565}
566
567#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
569#[serde(rename_all = "snake_case")]
570#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
571#[cfg_attr(feature = "codegen", ts(export))]
572pub enum LabelPos {
573 #[default]
574 Above,
575 Below,
576 Left,
577 Right,
578}
579
580#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582#[serde(deny_unknown_fields)]
583#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
584#[cfg_attr(feature = "codegen", ts(export))]
585pub struct LabelPlacement {
586 pub id: String,
588 #[serde(default, skip_serializing_if = "Option::is_none")]
589 #[cfg_attr(feature = "codegen", ts(optional))]
590 pub pos: Option<LabelPos>,
591}
592
593impl LabelPlacement {
594 pub fn pos_or_default(&self) -> LabelPos {
595 self.pos.unwrap_or(LabelPos::Above)
596 }
597}
598
599#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(untagged)]
602#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
603#[cfg_attr(feature = "codegen", ts(export))]
604pub enum LabelItem {
605 Id(String),
606 Placed(LabelPlacement),
607}
608
609impl LabelItem {
610 pub fn id(&self) -> &str {
611 match self {
612 LabelItem::Id(s) => s.as_str(),
613 LabelItem::Placed(p) => p.id.as_str(),
614 }
615 }
616
617 pub fn pos(&self) -> LabelPos {
618 match self {
619 LabelItem::Id(_) => LabelPos::Above,
620 LabelItem::Placed(p) => p.pos_or_default(),
621 }
622 }
623}
624
625#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
627#[serde(deny_unknown_fields)]
628#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
629#[cfg_attr(feature = "codegen", ts(export))]
630pub struct LabelLanes {
631 #[serde(default, skip_serializing_if = "Vec::is_empty")]
632 pub above: Vec<String>,
633 #[serde(default, skip_serializing_if = "Vec::is_empty")]
634 pub below: Vec<String>,
635 #[serde(default, skip_serializing_if = "Vec::is_empty")]
636 pub left: Vec<String>,
637 #[serde(default, skip_serializing_if = "Vec::is_empty")]
638 pub right: Vec<String>,
639}
640
641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
650#[serde(untagged)]
651#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
652#[cfg_attr(feature = "codegen", ts(export))]
653pub enum Label {
654 Id(String),
656 Items(Vec<LabelItem>),
658 Placed(LabelPlacement),
660 Lanes(LabelLanes),
662}
663
664impl Label {
665 pub fn placements(&self) -> Vec<(String, LabelPos)> {
667 match self {
668 Label::Id(id) => vec![(id.clone(), LabelPos::Above)],
669 Label::Items(items) => items
670 .iter()
671 .map(|it| (it.id().to_string(), it.pos()))
672 .collect(),
673 Label::Placed(p) => vec![(p.id.clone(), p.pos_or_default())],
674 Label::Lanes(lanes) => {
675 let mut out = Vec::new();
676 for id in &lanes.above {
677 out.push((id.clone(), LabelPos::Above));
678 }
679 for id in &lanes.below {
680 out.push((id.clone(), LabelPos::Below));
681 }
682 for id in &lanes.left {
683 out.push((id.clone(), LabelPos::Left));
684 }
685 for id in &lanes.right {
686 out.push((id.clone(), LabelPos::Right));
687 }
688 out
689 }
690 }
691 }
692}
693
694#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(untagged)]
699#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
700#[cfg_attr(feature = "codegen", ts(export))]
701pub enum MolIds {
702 One(String),
703 Many(Vec<String>),
704}
705
706impl MolIds {
707 pub fn as_slice(&self) -> &[String] {
708 match self {
709 MolIds::One(s) => std::slice::from_ref(s),
710 MolIds::Many(v) => v.as_slice(),
711 }
712 }
713
714 pub fn iter(&self) -> impl Iterator<Item = &str> {
715 self.as_slice().iter().map(String::as_str)
716 }
717
718 pub fn is_empty(&self) -> bool {
719 match self {
720 MolIds::One(s) => s.trim().is_empty(),
721 MolIds::Many(v) => v.is_empty() || v.iter().all(|s| s.trim().is_empty()),
722 }
723 }
724
725 pub fn len(&self) -> usize {
726 self.as_slice().len()
727 }
728}
729
730#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
735#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
736#[cfg_attr(feature = "codegen", ts(export))]
737pub struct EdgeNode {
738 #[serde(rename = "type")]
740 #[cfg_attr(feature = "codegen", ts(rename = "type"))]
741 pub type_: EdgeNodeKind,
742 #[serde(alias = "source")]
744 pub sources: MolIds,
745 #[serde(alias = "target")]
747 pub targets: MolIds,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
753 #[cfg_attr(feature = "codegen", ts(optional))]
754 pub label: Option<Label>,
755 #[serde(default, skip_serializing_if = "Option::is_none")]
757 #[cfg_attr(feature = "codegen", ts(optional))]
758 pub edge_routing: Option<EdgeRouting>,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
761 #[cfg_attr(feature = "codegen", ts(optional))]
762 pub role: Option<String>,
763 #[serde(default)]
764 pub arrow: EdgeArrow,
765 #[serde(default, skip_serializing_if = "Option::is_none")]
766 #[cfg_attr(feature = "codegen", ts(optional))]
767 pub color: Option<String>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 #[cfg_attr(feature = "codegen", ts(optional))]
770 pub stroke_width: Option<f64>,
771 #[serde(default)]
772 pub dashed: bool,
773}
774
775impl EdgeNode {
776 pub fn edge_routing_or(&self, scheme: &LayoutOpts) -> EdgeRouting {
778 self.edge_routing
779 .unwrap_or_else(|| scheme.edge_routing_or_default())
780 }
781}
782
783#[derive(Debug, Clone, Serialize, Deserialize)]
789#[serde(untagged)]
790#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
791#[cfg_attr(feature = "codegen", ts(export))]
792pub enum Node {
793 Mol(MolNode),
794 Edge(EdgeNode),
795 Text(TextNode),
796}
797
798impl Node {
799 pub fn as_mol(&self) -> Option<&MolNode> {
800 match self {
801 Node::Mol(m) => Some(m),
802 _ => None,
803 }
804 }
805
806 pub fn as_edge(&self) -> Option<&EdgeNode> {
807 match self {
808 Node::Edge(e) => Some(e),
809 _ => None,
810 }
811 }
812
813 pub fn as_text(&self) -> Option<&TextNode> {
814 match self {
815 Node::Text(t) => Some(t),
816 _ => None,
817 }
818 }
819
820 pub fn id(&self) -> Option<&str> {
822 match self {
823 Node::Mol(m) => m.id.as_deref().map(str::trim).filter(|s| !s.is_empty()),
824 Node::Text(t) => t.id.as_deref().map(str::trim).filter(|s| !s.is_empty()),
825 Node::Edge(_) => None,
826 }
827 }
828}
829
830#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
836#[serde(rename_all = "snake_case")]
837#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
838#[cfg_attr(feature = "codegen", ts(export))]
839pub enum LayoutDirection {
840 #[default]
841 Right,
842 Left,
843 Up,
844 Down,
845}
846
847#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
849#[serde(rename_all = "snake_case")]
850#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
851#[cfg_attr(feature = "codegen", ts(export))]
852pub enum EdgeRouting {
853 Orthogonal,
854 #[default]
855 Polyline,
856 Splines,
857}
858
859#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
867#[serde(rename_all = "snake_case")]
868#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
869#[cfg_attr(feature = "codegen", ts(export))]
870pub enum LayoutAlgorithm {
871 #[default]
872 Layered,
873 Radial,
874 Force,
875 Stress,
876}
877
878#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
889#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
890#[cfg_attr(feature = "codegen", ts(export))]
891pub struct LayoutOpts {
892 #[serde(default, skip_serializing_if = "Option::is_none")]
894 #[cfg_attr(feature = "codegen", ts(optional))]
895 pub algorithm: Option<LayoutAlgorithm>,
896 #[serde(default, skip_serializing_if = "Option::is_none")]
898 #[cfg_attr(feature = "codegen", ts(optional))]
899 pub direction: Option<LayoutDirection>,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
902 #[cfg_attr(feature = "codegen", ts(optional))]
903 pub edge_routing: Option<EdgeRouting>,
904 #[serde(default, skip_serializing_if = "Option::is_none")]
906 #[cfg_attr(feature = "codegen", ts(optional))]
907 pub node_spacing: Option<f64>,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
910 #[cfg_attr(feature = "codegen", ts(optional))]
911 pub layer_spacing: Option<f64>,
912}
913
914impl LayoutOpts {
915 pub const DEFAULT_NODE_SPACING: f64 = 20.0;
917 pub const DEFAULT_LAYER_SPACING: f64 = 0.0;
919
920 pub fn algorithm_or_default(&self) -> LayoutAlgorithm {
921 self.algorithm.unwrap_or_default()
922 }
923
924 pub fn direction_or_default(&self) -> LayoutDirection {
925 self.direction.unwrap_or_default()
926 }
927
928 pub fn edge_routing_or_default(&self) -> EdgeRouting {
929 self.edge_routing.unwrap_or_default()
930 }
931
932 pub fn node_spacing_or_default(&self) -> f64 {
933 self.node_spacing.unwrap_or(Self::DEFAULT_NODE_SPACING)
934 }
935
936 pub fn layer_spacing_or_default(&self) -> f64 {
937 self.layer_spacing.unwrap_or(Self::DEFAULT_LAYER_SPACING)
938 }
939}
940
941#[derive(Debug, Clone, Serialize, Deserialize)]
943#[serde(tag = "type", rename_all = "snake_case")]
944#[cfg_attr(feature = "codegen", derive(JsonSchema, TS))]
945#[cfg_attr(feature = "codegen", ts(export))]
946pub enum DepictSpec {
947 Mol {
948 #[serde(default, skip_serializing_if = "Option::is_none")]
949 #[cfg_attr(feature = "codegen", ts(optional))]
950 smiles: Option<String>,
951 #[serde(default, skip_serializing_if = "Option::is_none")]
952 #[cfg_attr(feature = "codegen", ts(optional))]
953 cxsmiles: Option<String>,
954 #[serde(default, skip_serializing_if = "Option::is_none")]
955 #[cfg_attr(feature = "codegen", ts(optional))]
956 molfile: Option<String>,
957 #[serde(default, skip_serializing_if = "Option::is_none")]
958 #[cfg_attr(feature = "codegen", ts(optional))]
959 id: Option<String>,
960 #[serde(default, skip_serializing_if = "Option::is_none")]
961 #[cfg_attr(feature = "codegen", ts(optional))]
962 shade: Option<ShadeSpec>,
963 #[serde(default, skip_serializing_if = "Option::is_none")]
964 #[cfg_attr(feature = "codegen", ts(optional))]
965 star_labels: Option<Vec<Option<String>>>,
966 #[serde(default, skip_serializing_if = "Option::is_none")]
967 #[cfg_attr(feature = "codegen", ts(optional))]
968 align_to: Option<AlignTo>,
969 #[serde(default, skip_serializing_if = "Option::is_none")]
971 #[cfg_attr(feature = "codegen", ts(optional))]
972 label: Option<Label>,
973 #[serde(default, skip_serializing_if = "Option::is_none")]
974 #[cfg_attr(feature = "codegen", ts(optional))]
975 color: Option<String>,
976 #[serde(default, skip_serializing_if = "Option::is_none")]
977 #[cfg_attr(feature = "codegen", ts(optional))]
978 scale: Option<f64>,
979 #[serde(default, skip_serializing_if = "Option::is_none")]
980 #[cfg_attr(feature = "codegen", ts(optional))]
981 weight: Option<f64>,
982 #[serde(default, skip_serializing_if = "Option::is_none")]
983 #[cfg_attr(feature = "codegen", ts(optional))]
984 halo: Option<bool>,
985 #[serde(default, skip_serializing_if = "Option::is_none")]
986 #[cfg_attr(feature = "codegen", ts(optional))]
987 opts: Option<Opts>,
988 },
989 Group {
990 #[serde(default, skip_serializing_if = "Option::is_none")]
991 #[cfg_attr(feature = "codegen", ts(optional))]
992 id: Option<String>,
993 #[serde(default)]
995 align: bool,
996 #[serde(default)]
998 children: Vec<Node>,
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1001 #[cfg_attr(feature = "codegen", ts(optional))]
1002 opts: Option<Opts>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 #[cfg_attr(feature = "codegen", ts(optional))]
1005 color: Option<String>,
1006 #[serde(default, skip_serializing_if = "Option::is_none")]
1007 #[cfg_attr(feature = "codegen", ts(optional))]
1008 scale: Option<f64>,
1009 },
1010 ReactionScheme {
1012 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 #[cfg_attr(feature = "codegen", ts(optional))]
1014 id: Option<String>,
1015 #[serde(default)]
1017 children: Vec<Node>,
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1020 #[cfg_attr(feature = "codegen", ts(optional))]
1021 layout: Option<LayoutOpts>,
1022 #[serde(default, skip_serializing_if = "Option::is_none")]
1024 #[cfg_attr(feature = "codegen", ts(optional))]
1025 opts: Option<Opts>,
1026 #[serde(default, skip_serializing_if = "Option::is_none")]
1027 #[cfg_attr(feature = "codegen", ts(optional))]
1028 color: Option<String>,
1029 #[serde(default, skip_serializing_if = "Option::is_none")]
1030 #[cfg_attr(feature = "codegen", ts(optional))]
1031 scale: Option<f64>,
1032 },
1033}
1034
1035impl DepictSpec {
1036 pub fn mols(&self) -> Vec<MolNode> {
1038 match self {
1039 DepictSpec::Mol {
1040 smiles,
1041 cxsmiles,
1042 molfile,
1043 id,
1044 shade,
1045 star_labels,
1046 align_to,
1047 label,
1048 color,
1049 scale,
1050 weight,
1051 halo,
1052 opts,
1053 } => vec![MolNode {
1054 type_: MolNodeKind::Mol,
1055 smiles: smiles.clone(),
1056 cxsmiles: cxsmiles.clone(),
1057 molfile: molfile.clone(),
1058 id: id.clone(),
1059 shade: shade.clone(),
1060 star_labels: star_labels.clone(),
1061 align_to: align_to.clone(),
1062 label: label.clone(),
1063 color: color.clone(),
1064 scale: *scale,
1065 weight: *weight,
1066 halo: *halo,
1067 opts: opts.clone(),
1068 }],
1069 DepictSpec::Group { children, .. }
1070 | DepictSpec::ReactionScheme { children, .. } => children
1071 .iter()
1072 .filter_map(Node::as_mol)
1073 .cloned()
1074 .collect(),
1075 }
1076 }
1077
1078 pub fn nodes(&self) -> &[Node] {
1080 match self {
1081 DepictSpec::Group { children, .. }
1082 | DepictSpec::ReactionScheme { children, .. } => children.as_slice(),
1083 DepictSpec::Mol { .. } => &[],
1084 }
1085 }
1086
1087 pub fn edges(&self) -> Vec<&EdgeNode> {
1089 self.nodes().iter().filter_map(Node::as_edge).collect()
1090 }
1091
1092 pub fn texts(&self) -> Vec<&TextNode> {
1094 self.nodes().iter().filter_map(Node::as_text).collect()
1095 }
1096
1097 pub fn node_by_id(&self, id: &str) -> Option<&Node> {
1099 self.nodes().iter().find(|n| n.id() == Some(id))
1100 }
1101
1102 pub fn align_enabled(&self) -> bool {
1103 matches!(self, DepictSpec::Group { align: true, .. })
1104 }
1105
1106 pub fn container_opts(&self) -> Option<&Opts> {
1108 match self {
1109 DepictSpec::Group { opts, .. } | DepictSpec::ReactionScheme { opts, .. } => {
1110 opts.as_ref()
1111 }
1112 DepictSpec::Mol { .. } => None,
1113 }
1114 }
1115
1116 #[inline]
1118 pub fn group_opts(&self) -> Option<&Opts> {
1119 match self {
1120 DepictSpec::Group { opts, .. } => opts.as_ref(),
1121 _ => None,
1122 }
1123 }
1124
1125 pub fn resolve_mol_chrome(&self, i: usize) -> MolOpts {
1130 let mols = self.mols();
1131 let node = mols.get(i).expect("mol index");
1132 let mut o = MolOpts::default();
1133 if let Some(bag) = self.container_opts() {
1134 bag.apply_to_mol(&mut o);
1135 }
1136 match self {
1137 DepictSpec::Group { color, scale, .. }
1138 | DepictSpec::ReactionScheme { color, scale, .. } => {
1139 o.merge_common(&CommonOpts {
1140 color: color.clone(),
1141 scale: *scale,
1142 });
1143 }
1144 DepictSpec::Mol { .. } => {}
1145 }
1146 if let Some(ref bag) = node.opts {
1147 bag.apply_to_mol(&mut o);
1148 }
1149 o.merge_from(&node.local_opts());
1150 o
1151 }
1152}