cosmol-viewer 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cosmol-viewer might be problematic. Click here for more details.

Files changed (36) hide show
  1. {cosmol_viewer-0.1.0 → cosmol_viewer-0.1.1}/Cargo.lock +143 -12
  2. cosmol_viewer-0.1.1/Cargo.toml +22 -0
  3. cosmol_viewer-0.1.1/PKG-INFO +57 -0
  4. cosmol_viewer-0.1.1/crates/core/Cargo.toml +17 -0
  5. cosmol_viewer-0.1.1/crates/core/src/lib.rs +72 -0
  6. cosmol_viewer-0.1.1/crates/core/src/parser/mod.rs +2 -0
  7. cosmol_viewer-0.1.1/crates/core/src/parser/sdf.rs +254 -0
  8. cosmol_viewer-0.1.1/crates/core/src/scene.rs +63 -0
  9. cosmol_viewer-0.1.0/cosmol_viewer_core/src/shader/app.rs → cosmol_viewer-0.1.1/crates/core/src/shader/canvas.rs +127 -86
  10. cosmol_viewer-0.1.1/crates/core/src/shader/fragment.glsl +34 -0
  11. cosmol_viewer-0.1.1/crates/core/src/shader/mod.rs +3 -0
  12. {cosmol_viewer-0.1.0/cosmol_viewer_core → cosmol_viewer-0.1.1/crates/core}/src/shader/vertex.glsl +1 -1
  13. cosmol_viewer-0.1.1/crates/core/src/shapes/mod.rs +3 -0
  14. cosmol_viewer-0.1.1/crates/core/src/shapes/molecules.rs +273 -0
  15. cosmol_viewer-0.1.1/crates/core/src/shapes/sphere.rs +246 -0
  16. cosmol_viewer-0.1.1/crates/core/src/shapes/stick.rs +152 -0
  17. cosmol_viewer-0.1.1/crates/core/src/utils.rs +85 -0
  18. {cosmol_viewer-0.1.0/cosmol_viewer_python → cosmol_viewer-0.1.1/crates/python}/Cargo.toml +6 -3
  19. cosmol_viewer-0.1.1/crates/python/README.md +49 -0
  20. cosmol_viewer-0.1.1/crates/python/build.rs +22 -0
  21. cosmol_viewer-0.1.1/crates/python/src/lib.rs +354 -0
  22. cosmol_viewer-0.1.1/crates/python/src/parser.rs +24 -0
  23. cosmol_viewer-0.1.1/crates/python/src/shapes.rs +102 -0
  24. cosmol_viewer-0.1.1/pyproject.toml +16 -0
  25. cosmol_viewer-0.1.0/Cargo.toml +0 -15
  26. cosmol_viewer-0.1.0/PKG-INFO +0 -5
  27. cosmol_viewer-0.1.0/cosmol_viewer_core/Cargo.toml +0 -11
  28. cosmol_viewer-0.1.0/cosmol_viewer_core/src/lib.rs +0 -113
  29. cosmol_viewer-0.1.0/cosmol_viewer_core/src/shader/fragment.glsl +0 -30
  30. cosmol_viewer-0.1.0/cosmol_viewer_core/src/shader/mod.rs +0 -3
  31. cosmol_viewer-0.1.0/cosmol_viewer_core/src/utils.rs +0 -167
  32. cosmol_viewer-0.1.0/cosmol_viewer_python/build.rs +0 -22
  33. cosmol_viewer-0.1.0/cosmol_viewer_python/src/lib.rs +0 -150
  34. cosmol_viewer-0.1.0/pyproject.toml +0 -14
  35. {cosmol_viewer-0.1.0/cosmol_viewer_core → cosmol_viewer-0.1.1/crates/core}/src/shader/bg_fragment.glsl +0 -0
  36. {cosmol_viewer-0.1.0/cosmol_viewer_core → cosmol_viewer-0.1.1/crates/core}/src/shader/bg_vertex.glsl +0 -0
@@ -431,6 +431,15 @@ version = "0.22.1"
431
431
  source = "registry+https://github.com/rust-lang/crates.io-index"
432
432
  checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
433
433
 
434
+ [[package]]
435
+ name = "bincode"
436
+ version = "1.3.3"
437
+ source = "registry+https://github.com/rust-lang/crates.io-index"
438
+ checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
439
+ dependencies = [
440
+ "serde",
441
+ ]
442
+
434
443
  [[package]]
435
444
  name = "bit-set"
436
445
  version = "0.8.0"
@@ -688,40 +697,78 @@ dependencies = [
688
697
  "libc",
689
698
  ]
690
699
 
700
+ [[package]]
701
+ name = "cosmol_viewer"
702
+ version = "0.1.1"
703
+ dependencies = [
704
+ "bytemuck",
705
+ "cosmol_viewer_core",
706
+ "eframe",
707
+ "egui_extras",
708
+ "hex",
709
+ "ipc-channel",
710
+ "serde",
711
+ "serde_json",
712
+ "sha2",
713
+ "wasm-bindgen-futures",
714
+ "web-sys",
715
+ ]
716
+
691
717
  [[package]]
692
718
  name = "cosmol_viewer_core"
693
- version = "0.1.0"
719
+ version = "0.1.1"
694
720
  dependencies = [
695
721
  "bytemuck",
696
722
  "eframe",
697
723
  "egui_extras",
698
724
  "glam",
725
+ "once_cell",
726
+ "serde",
727
+ "serde_json",
728
+ "serde_repr",
729
+ "wasm-bindgen-futures",
730
+ "web-sys",
731
+ ]
732
+
733
+ [[package]]
734
+ name = "cosmol_viewer_gui"
735
+ version = "0.1.1"
736
+ dependencies = [
737
+ "bytemuck",
738
+ "cosmol_viewer_core",
739
+ "eframe",
740
+ "egui_extras",
741
+ "ipc-channel",
699
742
  "serde",
743
+ "serde_json",
744
+ "wasm-bindgen-futures",
745
+ "web-sys",
700
746
  ]
701
747
 
702
748
  [[package]]
703
749
  name = "cosmol_viewer_python"
704
- version = "0.1.0"
750
+ version = "0.0.0"
705
751
  dependencies = [
706
752
  "base64 0.22.1",
707
753
  "cosmol_viewer_core",
708
754
  "eframe",
709
755
  "egui_extras",
710
- "glam",
756
+ "hex",
757
+ "ipc-channel",
711
758
  "pyo3",
712
759
  "serde_json",
760
+ "sha2",
713
761
  "uuid",
714
762
  "wasm-bindgen",
715
763
  ]
716
764
 
717
765
  [[package]]
718
766
  name = "cosmol_viewer_wasm"
719
- version = "0.1.0"
767
+ version = "0.1.1"
720
768
  dependencies = [
721
769
  "cosmol_viewer_core",
722
770
  "eframe",
723
771
  "egui_extras",
724
- "glam",
725
772
  "log",
726
773
  "serde_json",
727
774
  "wasm-bindgen",
@@ -747,6 +794,15 @@ dependencies = [
747
794
  "cfg-if",
748
795
  ]
749
796
 
797
+ [[package]]
798
+ name = "crossbeam-channel"
799
+ version = "0.5.15"
800
+ source = "registry+https://github.com/rust-lang/crates.io-index"
801
+ checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
802
+ dependencies = [
803
+ "crossbeam-utils",
804
+ ]
805
+
750
806
  [[package]]
751
807
  name = "crossbeam-utils"
752
808
  version = "0.8.21"
@@ -1132,6 +1188,12 @@ version = "0.9.0"
1132
1188
  source = "registry+https://github.com/rust-lang/crates.io-index"
1133
1189
  checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
1134
1190
 
1191
+ [[package]]
1192
+ name = "fnv"
1193
+ version = "1.0.7"
1194
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1195
+ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
1196
+
1135
1197
  [[package]]
1136
1198
  name = "foldhash"
1137
1199
  version = "0.1.5"
@@ -1603,6 +1665,24 @@ version = "2.0.6"
1603
1665
  source = "registry+https://github.com/rust-lang/crates.io-index"
1604
1666
  checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
1605
1667
 
1668
+ [[package]]
1669
+ name = "ipc-channel"
1670
+ version = "0.20.0"
1671
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1672
+ checksum = "5b1c98b70019c830a1fc39cecfe1f60ff99c4122f0a189697c810c90ec545c14"
1673
+ dependencies = [
1674
+ "bincode",
1675
+ "crossbeam-channel",
1676
+ "fnv",
1677
+ "libc",
1678
+ "mio",
1679
+ "rand 0.9.1",
1680
+ "serde",
1681
+ "tempfile",
1682
+ "uuid",
1683
+ "windows",
1684
+ ]
1685
+
1606
1686
  [[package]]
1607
1687
  name = "itoa"
1608
1688
  version = "1.0.15"
@@ -1826,6 +1906,17 @@ dependencies = [
1826
1906
  "simd-adler32",
1827
1907
  ]
1828
1908
 
1909
+ [[package]]
1910
+ name = "mio"
1911
+ version = "1.0.4"
1912
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1913
+ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
1914
+ dependencies = [
1915
+ "libc",
1916
+ "wasi 0.11.1+wasi-snapshot-preview1",
1917
+ "windows-sys 0.59.0",
1918
+ ]
1919
+
1829
1920
  [[package]]
1830
1921
  name = "naga"
1831
1922
  version = "24.0.0"
@@ -2318,7 +2409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2318
2409
  checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
2319
2410
  dependencies = [
2320
2411
  "phf_shared",
2321
- "rand",
2412
+ "rand 0.8.5",
2322
2413
  ]
2323
2414
 
2324
2415
  [[package]]
@@ -2579,8 +2670,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2579
2670
  checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
2580
2671
  dependencies = [
2581
2672
  "libc",
2582
- "rand_chacha",
2583
- "rand_core",
2673
+ "rand_chacha 0.3.1",
2674
+ "rand_core 0.6.4",
2675
+ ]
2676
+
2677
+ [[package]]
2678
+ name = "rand"
2679
+ version = "0.9.1"
2680
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2681
+ checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
2682
+ dependencies = [
2683
+ "rand_chacha 0.9.0",
2684
+ "rand_core 0.9.3",
2584
2685
  ]
2585
2686
 
2586
2687
  [[package]]
@@ -2590,7 +2691,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2590
2691
  checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
2591
2692
  dependencies = [
2592
2693
  "ppv-lite86",
2593
- "rand_core",
2694
+ "rand_core 0.6.4",
2695
+ ]
2696
+
2697
+ [[package]]
2698
+ name = "rand_chacha"
2699
+ version = "0.9.0"
2700
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2701
+ checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
2702
+ dependencies = [
2703
+ "ppv-lite86",
2704
+ "rand_core 0.9.3",
2594
2705
  ]
2595
2706
 
2596
2707
  [[package]]
@@ -2602,6 +2713,15 @@ dependencies = [
2602
2713
  "getrandom 0.2.16",
2603
2714
  ]
2604
2715
 
2716
+ [[package]]
2717
+ name = "rand_core"
2718
+ version = "0.9.3"
2719
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2720
+ checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
2721
+ dependencies = [
2722
+ "getrandom 0.3.3",
2723
+ ]
2724
+
2605
2725
  [[package]]
2606
2726
  name = "raw-window-handle"
2607
2727
  version = "0.6.2"
@@ -2805,6 +2925,17 @@ dependencies = [
2805
2925
  "digest",
2806
2926
  ]
2807
2927
 
2928
+ [[package]]
2929
+ name = "sha2"
2930
+ version = "0.10.9"
2931
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2932
+ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
2933
+ dependencies = [
2934
+ "cfg-if",
2935
+ "cpufeatures",
2936
+ "digest",
2937
+ ]
2938
+
2808
2939
  [[package]]
2809
2940
  name = "shlex"
2810
2941
  version = "1.3.0"
@@ -3027,12 +3158,12 @@ dependencies = [
3027
3158
 
3028
3159
  [[package]]
3029
3160
  name = "test"
3030
- version = "0.1.0"
3161
+ version = "0.1.1"
3031
3162
  dependencies = [
3163
+ "cosmol_viewer",
3032
3164
  "cosmol_viewer_core",
3033
3165
  "eframe",
3034
3166
  "egui_extras",
3035
- "glam",
3036
3167
  ]
3037
3168
 
3038
3169
  [[package]]
@@ -4242,7 +4373,7 @@ dependencies = [
4242
4373
  "hex",
4243
4374
  "nix",
4244
4375
  "ordered-stream",
4245
- "rand",
4376
+ "rand 0.8.5",
4246
4377
  "serde",
4247
4378
  "serde_repr",
4248
4379
  "sha1",
@@ -0,0 +1,22 @@
1
+ [workspace.package]
2
+ edition = "2024"
3
+ version = "0.1.1"
4
+ authors = ["9028 wjt@cosmol.org"]
5
+ repository = "https://github.com/COSMol-repl/COSMol-viewer"
6
+ homepage = "https://github.com/COSMol-repl/COSMol-viewer"
7
+ keywords = ["molecular", "visualization"]
8
+
9
+ [workspace]
10
+ resolver = "2"
11
+ members = ["crates/python"]
12
+
13
+ [workspace.dependencies]
14
+ cosmol_viewer = {path = "cosmol_viewer"}
15
+ cosmol_viewer_core = { path = "crates/core" }
16
+ eframe = { version = "0.31.1"}
17
+ egui_extras = { version = "0.31.1", features = ["svg"] }
18
+ serde = { version = "1.0.219" , features = ["derive"] }
19
+ serde_json = "1.0.140"
20
+ sha2 = "0.10.9"
21
+ hex = "0.4.3"
22
+ ipc-channel = "0.20.0"
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: cosmol-viewer
3
+ Version: 0.1.1
4
+ Summary: Molecular visualization tools
5
+ Author-email: 95028 <wjt@cosmol.org>
6
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
7
+ Project-URL: Repository, https://github.com/COSMol-repl/COSMol-viewer
8
+
9
+ # COSMol-viewer
10
+
11
+ A high-performance molecular visualization library built with Rust and WebGPU, designed for seamless integration into Python workflows.
12
+
13
+ - ⚡ Fast: Native-speed rendering powered by Rust and GPU acceleration
14
+
15
+ - 🧬 Flexible: Load molecules from .sdf, .pdb, and dynamically update 3D structures
16
+
17
+ - 📓 Notebook-friendly: Fully supports Jupyter and Google Colab — ideal for education, research, and live demos
18
+
19
+ - 🔁 Real-time updates: Update molecular coordinates on-the-fly for simulations or animations
20
+
21
+ - 🎨 Customizable: Control styles, camera, and rendering settings programmatically
22
+
23
+ # Installation
24
+
25
+ ```sh
26
+ pip install cosmol-viewer==0.1.1
27
+ ```
28
+
29
+ # Usage
30
+
31
+ ```python
32
+ from cosmol_viewer import Scene, Viewer, parse_sdf, Molecules
33
+
34
+ # === Step 1: Load and render a molecule ===
35
+ with open("molecule.sdf", "r") as f:
36
+ sdf = f.read()
37
+ mol = Molecules(parse_sdf(sdf)).centered()
38
+
39
+ scene = Scene()
40
+ scene.scale(0.1)
41
+ scene.add_shape(mol, "mol")
42
+
43
+ viewer = Viewer.render(scene) # Launch the viewer
44
+
45
+ # === Step 2: Update the same molecule dynamically ===
46
+ import time
47
+
48
+ for i in range(1, 10): # Simulate multiple frames
49
+ with open(f"frames/frame_{i}.sdf", "r") as f:
50
+ sdf = f.read()
51
+ updated_mol = Molecules(parse_sdf(sdf)).centered()
52
+
53
+ scene.update_shape("mol", updated_mol)
54
+ viewer.update(scene)
55
+
56
+ time.sleep(0.033) # ~30 FPS
57
+ ```
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "cosmol_viewer_core"
3
+ version.workspace = true
4
+ edition = "2024"
5
+ publish = false
6
+
7
+ [dependencies]
8
+ eframe.workspace = true
9
+ serde_json.workspace = true
10
+ glam = { version = "0.30.3" , features = ["serde"] }
11
+ egui_extras.workspace = true
12
+ serde.workspace = true
13
+ bytemuck = "1.23.1"
14
+ web-sys = "0.3.77"
15
+ serde_repr = "0.1"
16
+ wasm-bindgen-futures = "0.4.50"
17
+ once_cell = "1.21.3"
@@ -0,0 +1,72 @@
1
+ mod shader;
2
+ use std::{
3
+ sync::{Arc, Mutex},
4
+ };
5
+
6
+ pub mod utils;
7
+ pub mod parser;
8
+ pub use eframe::egui;
9
+
10
+ use eframe::egui::{Color32, Stroke};
11
+
12
+ use shader::Canvas;
13
+
14
+ pub use crate::utils::{Shape};
15
+ pub mod shapes;
16
+ use crate::{scene::Scene};
17
+
18
+ pub mod scene;
19
+
20
+ pub struct AppWrapper(pub Arc<Mutex<Option<App>>>);
21
+
22
+ impl eframe::App for AppWrapper {
23
+ fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
24
+ if let Some(app) = &mut *self.0.lock().unwrap() {
25
+ app.update(ctx, frame);
26
+ }
27
+ }
28
+ }
29
+
30
+ pub struct App {
31
+ canvas: Canvas,
32
+ gl: Option<Arc<eframe::glow::Context>>,
33
+ pub ctx: egui::Context,
34
+ }
35
+
36
+ impl App {
37
+ pub fn new(cc: &eframe::CreationContext<'_>, scene: Scene) -> Self {
38
+ let gl = cc.gl.clone();
39
+ let canvas = Canvas::new(gl.as_ref().unwrap().clone(), scene).unwrap();
40
+ App {
41
+ gl,
42
+ canvas,
43
+ ctx: cc.egui_ctx.clone(),
44
+ }
45
+ }
46
+
47
+ pub fn update_scene(&mut self, scene: Scene) {
48
+ self.canvas.update_scene(scene);
49
+ }
50
+ }
51
+
52
+ impl eframe::App for App {
53
+ fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
54
+ egui_extras::install_image_loaders(ctx);
55
+ egui::CentralPanel::default()
56
+ .frame(
57
+ egui::Frame::default()
58
+ .fill(Color32::from_rgb(48, 48, 48))
59
+ .inner_margin(0.0)
60
+ .outer_margin(0.0)
61
+ .stroke(Stroke::new(0.0, Color32::from_rgb(30, 200, 30))),
62
+ )
63
+ .show(ctx, |ui| {
64
+ ui.set_width(ui.available_width());
65
+ ui.set_height(ui.available_height());
66
+
67
+ self.canvas.custom_painting(ui);
68
+ });
69
+ }
70
+ }
71
+
72
+
@@ -0,0 +1,2 @@
1
+ pub mod sdf;
2
+
@@ -0,0 +1,254 @@
1
+ #[derive(Debug, Clone)]
2
+ pub struct Atom {
3
+ pub atom: String,
4
+ pub elem: String,
5
+ pub x: f32,
6
+ pub y: f32,
7
+ pub z: f32,
8
+ pub serial: usize,
9
+ pub index: usize,
10
+ pub hetflag: bool,
11
+ pub bonds: Vec<usize>,
12
+ pub bond_order: Vec<f32>,
13
+ pub properties: std::collections::HashMap<String, String>,
14
+ }
15
+
16
+ pub type Molecule = Vec<Atom>;
17
+ pub type MoleculeData = Vec<Molecule>;
18
+
19
+ #[derive(Default)]
20
+ pub struct ParserOptions {
21
+ pub keep_h: bool,
22
+ pub multimodel: bool,
23
+ pub onemol: bool,
24
+ }
25
+
26
+ pub fn parse_sdf(sdf: &str, options: &ParserOptions) -> MoleculeData {
27
+ let lines: Vec<&str> = sdf.lines().collect();
28
+ if lines.len() > 3 && lines[3].len() > 38 {
29
+ let version = lines[3][34..39].trim();
30
+ match version {
31
+ "V3000" => parse_v3000(lines, options),
32
+ _ => parse_v2000(lines, options),
33
+ }
34
+ } else {
35
+ vec![vec![]]
36
+ }
37
+ }
38
+
39
+ fn parse_v2000(mut lines: Vec<&str>, options: &ParserOptions) -> MoleculeData {
40
+ let model_count = count_models(&lines);
41
+ // 多个分子但用户没开启
42
+ if model_count > 0 && !options.multimodel {
43
+ panic!(
44
+ "Found multiple molecules but 'multimodel' is false. Please enable 'multimodel = true' to parse all molecules."
45
+ );
46
+ }
47
+
48
+ // 用户开启了但其实只有一个
49
+ if model_count == 0 && options.multimodel {
50
+ panic!(
51
+ "Only one molecule found, but 'multimodel = true' was set. Consider setting 'multimodel = false' to avoid confusion."
52
+ );
53
+ }
54
+
55
+ let mut molecules = vec![vec![]];
56
+ let mut current = 0;
57
+
58
+ while lines.len() >= 4 {
59
+ let header = lines[3];
60
+ let atom_count = header[0..3].trim().parse::<usize>().unwrap_or(0);
61
+ let bond_count = header[3..6].trim().parse::<usize>().unwrap_or(0);
62
+
63
+ if atom_count == 0 || lines.len() < 4 + atom_count + bond_count {
64
+ break;
65
+ }
66
+
67
+ let mut serial_to_index = vec![None; atom_count];
68
+ let mut offset = 4;
69
+ let start = molecules[current].len();
70
+
71
+ for i in 0..atom_count {
72
+ let line = lines[offset + i];
73
+ let elem = line[31..34].trim();
74
+ let elem_cap = capitalize(elem);
75
+ if elem_cap != "H" || options.keep_h {
76
+ let atom = Atom {
77
+ atom: elem_cap.clone(),
78
+ elem: elem_cap,
79
+ x: line[0..10].trim().parse().unwrap_or(0.0),
80
+ y: line[10..20].trim().parse().unwrap_or(0.0),
81
+ z: line[20..30].trim().parse().unwrap_or(0.0),
82
+ serial: start + i,
83
+ index: molecules[current].len(),
84
+ hetflag: true,
85
+ bonds: vec![],
86
+ bond_order: vec![],
87
+ properties: std::collections::HashMap::new(),
88
+ };
89
+ serial_to_index[i] = Some(molecules[current].len());
90
+ molecules[current].push(atom);
91
+ }
92
+ }
93
+
94
+ offset += atom_count;
95
+
96
+ for i in 0..bond_count {
97
+ let line = lines[offset + i];
98
+ let from = line[0..3]
99
+ .trim()
100
+ .parse::<usize>()
101
+ .unwrap_or(0)
102
+ .saturating_sub(1);
103
+ let to = line[3..6]
104
+ .trim()
105
+ .parse::<usize>()
106
+ .unwrap_or(0)
107
+ .saturating_sub(1);
108
+ let order = line[6..].trim().parse::<f32>().unwrap_or(1.0);
109
+ if let (Some(f), Some(t)) = (
110
+ serial_to_index.get(from).and_then(|x| *x),
111
+ serial_to_index.get(to).and_then(|x| *x),
112
+ ) {
113
+ molecules[current][f].bonds.push(t);
114
+ molecules[current][f].bond_order.push(order);
115
+ molecules[current][t].bonds.push(f);
116
+ molecules[current][t].bond_order.push(order);
117
+ }
118
+ }
119
+
120
+ let mut next_offset = offset + bond_count;
121
+ if options.multimodel {
122
+ if !options.onemol {
123
+ molecules.push(vec![]);
124
+ current += 1;
125
+ }
126
+ while next_offset < lines.len() && lines[next_offset] != "$$$$" {
127
+ next_offset += 1;
128
+ }
129
+ lines.drain(0..=next_offset);
130
+ } else {
131
+ break;
132
+ }
133
+ }
134
+
135
+ molecules
136
+ }
137
+
138
+ fn parse_v3000(mut lines: Vec<&str>, options: &ParserOptions) -> MoleculeData {
139
+ let model_count = count_models(&lines);
140
+
141
+ // 多个分子但用户没开启
142
+ if model_count > 0 && !options.multimodel {
143
+ panic!(
144
+ "Found multiple molecules but 'multimodel' is false. Please enable 'multimodel = true' to parse all molecules."
145
+ );
146
+ }
147
+
148
+ // 用户开启了但其实只有一个
149
+ if model_count == 0 && options.multimodel {
150
+ panic!(
151
+ "Only one molecule found, but 'multimodel = true' was set. Consider setting 'multimodel = false' to avoid confusion."
152
+ );
153
+ }
154
+
155
+ let mut molecules = vec![vec![]];
156
+ let mut current = 0;
157
+
158
+ while lines.len() >= 8 {
159
+ if !lines[4].starts_with("M V30 BEGIN CTAB") || !lines[5].starts_with("M V30 COUNTS") {
160
+ break;
161
+ }
162
+
163
+ let counts: Vec<_> = lines[5][13..].split_whitespace().collect();
164
+ let atom_count = counts
165
+ .get(0)
166
+ .and_then(|s| s.parse::<usize>().ok())
167
+ .unwrap_or(0);
168
+ let bond_count = counts
169
+ .get(1)
170
+ .and_then(|s| s.parse::<usize>().ok())
171
+ .unwrap_or(0);
172
+ let mut offset = 7;
173
+
174
+ let mut serial_to_index = vec![None; atom_count];
175
+ let start = molecules[current].len();
176
+
177
+ for i in 0..atom_count {
178
+ let line = lines[offset + i];
179
+ let parts: Vec<_> = line[6..].split_whitespace().collect();
180
+ if parts.len() > 4 {
181
+ let elem_cap = capitalize(parts[1]);
182
+ if elem_cap != "H" || options.keep_h {
183
+ let atom = Atom {
184
+ atom: elem_cap.clone(),
185
+ elem: elem_cap,
186
+ x: parts[2].parse().unwrap_or(0.0),
187
+ y: parts[3].parse().unwrap_or(0.0),
188
+ z: parts[4].parse().unwrap_or(0.0),
189
+ serial: start + i,
190
+ index: molecules[current].len(),
191
+ hetflag: true,
192
+ bonds: vec![],
193
+ bond_order: vec![],
194
+ properties: std::collections::HashMap::new(),
195
+ };
196
+ serial_to_index[i] = Some(molecules[current].len());
197
+ molecules[current].push(atom);
198
+ }
199
+ }
200
+ }
201
+
202
+ offset += atom_count + 1; // skip "END ATOM"
203
+ offset += 1; // BEGIN BOND
204
+
205
+ for i in 0..bond_count {
206
+ let line = lines[offset + i];
207
+ let parts: Vec<_> = line[6..].split_whitespace().collect();
208
+ if parts.len() > 3 {
209
+ let from = parts[2].parse::<usize>().unwrap_or(0).saturating_sub(1);
210
+ let to = parts[3].parse::<usize>().unwrap_or(0).saturating_sub(1);
211
+ let order = parts[1].parse::<f32>().unwrap_or(1.0);
212
+ if let (Some(f), Some(t)) = (
213
+ serial_to_index.get(from).and_then(|x| *x),
214
+ serial_to_index.get(to).and_then(|x| *x),
215
+ ) {
216
+ molecules[current][f].bonds.push(t);
217
+ molecules[current][f].bond_order.push(order);
218
+ molecules[current][t].bonds.push(f);
219
+ molecules[current][t].bond_order.push(order);
220
+ }
221
+ }
222
+ }
223
+
224
+ let mut next_offset = offset + bond_count;
225
+ if options.multimodel {
226
+ if !options.onemol {
227
+ molecules.push(vec![]);
228
+ current += 1;
229
+ }
230
+ while next_offset < lines.len() && lines[next_offset] != "$$$$" {
231
+ next_offset += 1;
232
+ }
233
+ lines.drain(0..=next_offset);
234
+ } else {
235
+ break;
236
+ }
237
+ }
238
+
239
+ molecules
240
+ }
241
+
242
+ fn capitalize(s: &str) -> String {
243
+ let mut chars = s.chars();
244
+ match chars.next() {
245
+ Some(first) => {
246
+ first.to_ascii_uppercase().to_string() + &chars.as_str().to_ascii_lowercase()
247
+ }
248
+ None => String::new(),
249
+ }
250
+ }
251
+
252
+ fn count_models(lines: &[&str]) -> usize {
253
+ lines.iter().filter(|line| line.trim() == "$$$$").count()
254
+ }