cosmol-viewer 0.1.2.dev5__tar.gz → 0.1.3.dev1__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.
Files changed (30) hide show
  1. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/Cargo.lock +25 -3
  2. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/Cargo.toml +3 -3
  3. cosmol_viewer-0.1.3.dev1/PKG-INFO +6 -0
  4. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/lib.rs +117 -5
  5. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/scene.rs +3 -3
  6. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/Cargo.toml +1 -0
  7. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/README.md +3 -0
  8. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/src/lib.rs +81 -24
  9. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/src/shapes.rs +2 -2
  10. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/wasm/Cargo.toml +3 -2
  11. cosmol_viewer-0.1.3.dev1/crates/wasm/src/lib.rs +420 -0
  12. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/pyproject.toml +1 -1
  13. cosmol_viewer-0.1.2.dev5/PKG-INFO +0 -70
  14. cosmol_viewer-0.1.2.dev5/crates/wasm/src/lib.rs +0 -257
  15. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/Cargo.toml +0 -0
  16. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/parser/mod.rs +0 -0
  17. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/parser/sdf.rs +0 -0
  18. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/bg_fragment.glsl +0 -0
  19. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/bg_vertex.glsl +0 -0
  20. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/canvas.rs +0 -0
  21. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/fragment.glsl +0 -0
  22. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/mod.rs +0 -0
  23. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/vertex.glsl +0 -0
  24. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shapes/mod.rs +0 -0
  25. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shapes/molecules.rs +0 -0
  26. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shapes/sphere.rs +0 -0
  27. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shapes/stick.rs +0 -0
  28. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/utils.rs +0 -0
  29. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/build.rs +0 -0
  30. {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/src/parser.rs +0 -0
@@ -727,7 +727,7 @@ dependencies = [
727
727
 
728
728
  [[package]]
729
729
  name = "cosmol_viewer"
730
- version = "0.1.2-nightly.5"
730
+ version = "0.1.3-nightly.1"
731
731
  dependencies = [
732
732
  "bytemuck",
733
733
  "cosmol_viewer_core",
@@ -740,7 +740,7 @@ dependencies = [
740
740
 
741
741
  [[package]]
742
742
  name = "cosmol_viewer_core"
743
- version = "0.1.2-nightly.5"
743
+ version = "0.1.3-nightly.1"
744
744
  dependencies = [
745
745
  "bytemuck",
746
746
  "eframe",
@@ -770,11 +770,12 @@ dependencies = [
770
770
 
771
771
  [[package]]
772
772
  name = "cosmol_viewer_wasm"
773
- version = "0.1.2-nightly.5"
773
+ version = "0.1.3-nightly.1"
774
774
  dependencies = [
775
775
  "base64",
776
776
  "cosmol_viewer_core",
777
777
  "eframe",
778
+ "gloo-timers",
778
779
  "log",
779
780
  "pyo3",
780
781
  "serde",
@@ -1255,6 +1256,15 @@ dependencies = [
1255
1256
  "percent-encoding",
1256
1257
  ]
1257
1258
 
1259
+ [[package]]
1260
+ name = "futures-channel"
1261
+ version = "0.3.31"
1262
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1263
+ checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
1264
+ dependencies = [
1265
+ "futures-core",
1266
+ ]
1267
+
1258
1268
  [[package]]
1259
1269
  name = "futures-core"
1260
1270
  version = "0.3.31"
@@ -1374,6 +1384,18 @@ dependencies = [
1374
1384
  "serde",
1375
1385
  ]
1376
1386
 
1387
+ [[package]]
1388
+ name = "gloo-timers"
1389
+ version = "0.3.0"
1390
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1391
+ checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
1392
+ dependencies = [
1393
+ "futures-channel",
1394
+ "futures-core",
1395
+ "js-sys",
1396
+ "wasm-bindgen",
1397
+ ]
1398
+
1377
1399
  [[package]]
1378
1400
  name = "glow"
1379
1401
  version = "0.16.0"
@@ -1,6 +1,6 @@
1
1
  [workspace.package]
2
2
  edition = "2024"
3
- version = "0.1.2-nightly.5"
3
+ version = "0.1.3-nightly.1"
4
4
  authors = ["9028 wjt@cosmol.org"]
5
5
  repository = "https://github.com/COSMol-repl/COSMol-viewer"
6
6
  homepage = "https://github.com/COSMol-repl/COSMol-viewer"
@@ -13,8 +13,8 @@ resolver = "2"
13
13
  members = ["crates/python"]
14
14
 
15
15
  [workspace.dependencies]
16
- cosmol_viewer = { version = "0.1.2-nightly.5", path = "cosmol_viewer"}
17
- cosmol_viewer_core = { version = "0.1.2-nightly.5", path = "crates/core" }
16
+ cosmol_viewer = { version = "0.1.3-nightly.1", path = "cosmol_viewer"}
17
+ cosmol_viewer_core = { version = "0.1.3-nightly.1", path = "crates/core" }
18
18
 
19
19
  eframe = { version = "0.32.0", features = ["wayland","x11"] }
20
20
  pyo3 = { version = "0.25.1", features = ["extension-module", "abi3-py37"] }
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: cosmol-viewer
3
+ Version: 0.1.3.dev1
4
+ Summary: Molecular visualization tools
5
+ Author-email: 95028 <wjt@cosmol.org>
6
+ Project-URL: Repository, https://github.com/COSMol-repl/COSMol-viewer
@@ -38,6 +38,23 @@ pub struct App {
38
38
  }
39
39
 
40
40
  impl App {
41
+ pub fn play(
42
+ cc: &eframe::CreationContext<'_>,
43
+ frames: Vec<Scene>,
44
+ interval: f32,
45
+ loop_: bool,
46
+ ) -> Self {
47
+ let gl = cc.gl.clone();
48
+ let canvas = Canvas::new(gl.as_ref().unwrap().clone(), frames[0].clone()).unwrap();
49
+ App {
50
+ gl,
51
+ canvas,
52
+ ctx: cc.egui_ctx.clone(),
53
+ screenshot_requested: false,
54
+ screenshot_result: None,
55
+ }
56
+ }
57
+
41
58
  pub fn new(cc: &eframe::CreationContext<'_>, scene: Scene) -> Self {
42
59
  let gl = cc.gl.clone();
43
60
  let canvas = Canvas::new(gl.as_ref().unwrap().clone(), scene).unwrap();
@@ -130,7 +147,7 @@ pub struct NativeGuiViewer {
130
147
  }
131
148
 
132
149
  impl NativeGuiViewer {
133
- pub fn render(scene: &Scene) -> Self {
150
+ pub fn render(scene: &Scene, width: f32, height: f32) -> Self {
134
151
  use std::{
135
152
  sync::{Arc, Mutex},
136
153
  thread,
@@ -142,7 +159,7 @@ impl NativeGuiViewer {
142
159
  egui::{Vec2, ViewportBuilder},
143
160
  };
144
161
 
145
- let viewport_size = scene.viewport.unwrap_or([800, 500]);
162
+ // let viewport_size = scene.viewport.unwrap_or([800, 500]);
146
163
 
147
164
  let app: Arc<Mutex<Option<App>>> = Arc::new(Mutex::new(None));
148
165
  let app_clone = Arc::clone(&app);
@@ -173,8 +190,7 @@ impl NativeGuiViewer {
173
190
  }));
174
191
 
175
192
  let native_options = NativeOptions {
176
- viewport: ViewportBuilder::default()
177
- .with_inner_size(Vec2::new(viewport_size[0] as f32, viewport_size[1] as f32)),
193
+ viewport: ViewportBuilder::default().with_inner_size(Vec2::new(width, height)),
178
194
  depth_buffer: 24,
179
195
  event_loop_builder,
180
196
  ..Default::default()
@@ -193,7 +209,7 @@ impl NativeGuiViewer {
193
209
  });
194
210
 
195
211
  // 等待 App 初始化完成
196
- let timeout_ms = 3000;
212
+ let timeout_ms = 30000;
197
213
  let mut waited = 0;
198
214
 
199
215
  loop {
@@ -244,4 +260,100 @@ impl NativeGuiViewer {
244
260
  std::thread::sleep(std::time::Duration::from_millis(100));
245
261
  }
246
262
  }
263
+
264
+ pub fn play(frames: Vec<Scene>, interval: f32, loops: i64, width: f32, height: f32) {
265
+ use std::{
266
+ sync::{Arc, Mutex},
267
+ thread,
268
+ };
269
+
270
+ #[cfg(not(target_arch = "wasm32"))]
271
+ use eframe::{
272
+ NativeOptions,
273
+ egui::{Vec2, ViewportBuilder},
274
+ };
275
+
276
+ let app: Arc<Mutex<Option<App>>> = Arc::new(Mutex::new(None));
277
+ let app_clone = Arc::clone(&app);
278
+
279
+ let scene_init = frames[0].clone();
280
+
281
+ #[cfg(not(target_arch = "wasm32"))]
282
+ thread::spawn(move || {
283
+ use std::process;
284
+
285
+ use eframe::{EventLoopBuilderHook, run_native};
286
+ let event_loop_builder: Option<EventLoopBuilderHook> =
287
+ Some(Box::new(|event_loop_builder| {
288
+ #[cfg(target_family = "windows")]
289
+ {
290
+ use egui_winit::winit::platform::windows::EventLoopBuilderExtWindows;
291
+ event_loop_builder.with_any_thread(true);
292
+ }
293
+ #[cfg(feature = "wayland")]
294
+ {
295
+ use egui_winit::winit::platform::wayland::EventLoopBuilderExtWayland;
296
+ event_loop_builder.with_any_thread(true);
297
+ }
298
+ #[cfg(feature = "x11")]
299
+ {
300
+ use egui_winit::winit::platform::x11::EventLoopBuilderExtX11;
301
+ event_loop_builder.with_any_thread(true);
302
+ }
303
+ }));
304
+
305
+ let native_options = NativeOptions {
306
+ viewport: ViewportBuilder::default().with_inner_size(Vec2::new(width, height)),
307
+ depth_buffer: 24,
308
+ event_loop_builder,
309
+ ..Default::default()
310
+ };
311
+
312
+ let _ = run_native(
313
+ "cosmol_viewer",
314
+ native_options,
315
+ Box::new(move |cc| {
316
+ let mut guard = app_clone.lock().unwrap();
317
+ *guard = Some(App::new(cc, scene_init));
318
+ Ok(Box::new(AppWrapper(app_clone.clone())))
319
+ }),
320
+ );
321
+ process::exit(0);
322
+ });
323
+
324
+ // 等待 App 初始化完成
325
+ let timeout_ms = 30000;
326
+ let mut waited = 0;
327
+
328
+ loop {
329
+ if app.lock().unwrap().is_some() {
330
+ break;
331
+ }
332
+ if waited > timeout_ms {
333
+ panic!("Fail to initialize App");
334
+ }
335
+ thread::sleep(Duration::from_millis(10));
336
+ waited += 10;
337
+ }
338
+
339
+ let mut count = 0;
340
+ loop {
341
+ if loops >= 0 && count >= loops {
342
+ break;
343
+ }
344
+ count += 1;
345
+ for frame in &frames {
346
+ {
347
+ let mut guard = app.lock().unwrap();
348
+ if let Some(app) = &mut *guard {
349
+ app.update_scene(frame.clone());
350
+ app.ctx.request_repaint();
351
+ }
352
+ }
353
+ thread::sleep(Duration::from_secs_f32(interval));
354
+ }
355
+ }
356
+
357
+ // Self { app }
358
+ }
247
359
  }
@@ -66,9 +66,9 @@ impl Scene {
66
66
  }
67
67
  }
68
68
 
69
- pub fn set_viewport(&mut self, width: usize, height: usize) {
70
- self.viewport = Some([width, height]);
71
- }
69
+ // pub fn set_viewport(&mut self, width: usize, height: usize) {
70
+ // self.viewport = Some([width, height]);
71
+ // }
72
72
 
73
73
  pub fn set_background_color(&mut self, background_color: [f32; 3]) {
74
74
  self.background_color = background_color;
@@ -2,6 +2,7 @@
2
2
  name = "cosmol_viewer_python"
3
3
  edition.workspace = true
4
4
  publish = false
5
+ readme = "README.md"
5
6
 
6
7
  [dependencies]
7
8
  cosmol_viewer_core.workspace = true
@@ -7,6 +7,9 @@
7
7
  <a href="https://pypi.org/project/cosmol_viewer/">
8
8
  <img src="https://img.shields.io/pypi/v/cosmol_viewer.svg" alt="PyPi Latest Release"/>
9
9
  </a>
10
+ <a href="https://cosmol-repl.github.io/COSMol-viewer">
11
+ <img src="https://img.shields.io/badge/docs-latest-blue.svg" alt="Documentation Status"/>
12
+ </a>
10
13
  </div>
11
14
 
12
15
  A high-performance molecular visualization library built with Rust and WebGPU, designed for seamless integration into Python workflows.
@@ -9,10 +9,12 @@ use crate::{
9
9
  };
10
10
  use cosmol_viewer_core::{NativeGuiViewer, scene::Scene as _Scene};
11
11
  use cosmol_viewer_wasm::{WasmViewer, setup_wasm_if_needed};
12
+ use std::borrow::Borrow;
12
13
 
13
14
  mod parser;
14
15
  mod shapes;
15
16
 
17
+ #[derive(Clone)]
16
18
  #[pyclass]
17
19
  /// A 3D scene container for visualizing molecular or geometric shapes.
18
20
  ///
@@ -107,20 +109,6 @@ impl Scene {
107
109
  self.inner.delete_shape(id);
108
110
  }
109
111
 
110
- /// Set the viewport size of the scene.
111
- ///
112
- /// # Arguments
113
- ///
114
- /// * `width` - Width of the viewport in pixels.
115
- /// * `height` - Height of the viewport in pixels.
116
- ///
117
- /// # Example
118
- /// ```python
119
- /// scene.set_viewport(600, 400)
120
- /// ```
121
- pub fn set_viewport(&mut self, width: usize, height: usize) {
122
- self.inner.set_viewport(width, height);
123
- }
124
112
 
125
113
  /// Sets the global scale factor of the scene.
126
114
  ///
@@ -186,9 +174,9 @@ impl std::fmt::Display for RuntimeEnv {
186
174
  ///
187
175
  /// Use `Viewer.render(scene)` to create and display a viewer instance.
188
176
  ///
189
- /// Examples:
177
+ /// # Examples:
190
178
  /// ```python
191
- /// from cosmol import Viewer, Scene, Sphere
179
+ /// from cosmol_viewer import Viewer, Scene, Sphere
192
180
  /// scene = Scene()
193
181
  /// scene.add_shape(Sphere(...))
194
182
  /// viewer = Viewer.render(scene)
@@ -265,32 +253,34 @@ impl Viewer {
265
253
  ///
266
254
  /// Args:
267
255
  /// scene (Scene): The scene to render.
256
+ /// width (float): The width of the viewport in pixels.
257
+ /// height (float): The height of the viewport in pixels.
268
258
  ///
269
259
  /// Returns:
270
260
  /// Viewer: The created viewer instance.
271
261
  ///
272
262
  /// Examples:
273
263
  /// ```python
274
- /// from cosmol cosmol_viewer Viewer, Scene, Sphere
264
+ /// from cosmol_viewer import Viewer, Scene, Sphere
275
265
  ///
276
266
  /// scene = Scene()
277
267
  /// scene.add_shape(Sphere(center=[0.0, 0.0, 0.0], radius=1.0))
278
268
  ///
279
- /// viewer = Viewer.render(scene)
269
+ /// viewer = Viewer.render(scene, 800.0, 500.0)
280
270
  /// ```
281
- pub fn render(scene: &Scene, py: Python) -> Self {
271
+ pub fn render(scene: &Scene, width: f32, height: f32, py: Python) -> Self {
282
272
  let env_type = detect_runtime_env(py).unwrap();
283
273
  match env_type {
284
274
  RuntimeEnv::Colab | RuntimeEnv::Jupyter => {
285
275
  print_to_notebook(
286
276
  c_str!(
287
277
  r#"from IPython.display import display, HTML
288
- display(HTML("<div style='color:red;font-weight:bold;'>⚠️ Note: When running in Jupyter or Colab, animation updates may be limited by the notebook's output capacity, which can cause incomplete or delayed rendering.</div>"))"#
278
+ display(HTML("<div style='color:red;font-weight:bold;font-size:1rem;'>⚠️ Note: When running in Jupyter or Colab, animation updates may be limited by the notebook's output capacity, which can cause incomplete or delayed rendering.</div>"))"#
289
279
  ),
290
280
  py,
291
281
  );
292
282
  setup_wasm_if_needed(py);
293
- let wasm_viewer = WasmViewer::initate_viewer(py, &scene.inner);
283
+ let wasm_viewer = WasmViewer::initate_viewer(py, &scene.inner, width, height);
294
284
 
295
285
  Viewer {
296
286
  environment: env_type,
@@ -301,12 +291,71 @@ display(HTML("<div style='color:red;font-weight:bold;'>⚠️ Note: When running
301
291
  RuntimeEnv::PlainScript | RuntimeEnv::IPythonTerminal => Viewer {
302
292
  environment: env_type,
303
293
  wasm_viewer: None,
304
- native_gui_viewer: Some(NativeGuiViewer::render(&scene.inner)),
294
+ native_gui_viewer: Some(NativeGuiViewer::render(&scene.inner, width, height)),
305
295
  },
306
296
  _ => panic!("Error: Invalid runtime environment"),
307
297
  }
308
298
  }
309
299
 
300
+ #[staticmethod]
301
+ /// Render a 3D scene based on the current environment.
302
+ ///
303
+ /// If running inside Jupyter or Colab, the scene will be displayed inline using WebAssembly.
304
+ /// If running from a script or terminal, a native GUI window is used (if supported).
305
+ ///
306
+ /// Args:
307
+ /// scene (Scene): The scene to render.
308
+ /// width (float): The width of the viewport in pixels.
309
+ /// height (float): The height of the viewport in pixels.
310
+ ///
311
+ /// Returns:
312
+ /// Viewer: The created viewer instance.
313
+ ///
314
+ /// Examples:
315
+ /// ```python
316
+ /// from cosmol_viewer import Viewer, Scene, Sphere
317
+ ///
318
+ /// scene = Scene()
319
+ /// scene.add_shape(Sphere(center=[0.0, 0.0, 0.0], radius=1.0))
320
+ ///
321
+ /// viewer = Viewer.render(scene, 800.0, 500.0)
322
+ /// ```
323
+ pub fn play(
324
+ frames: Vec<Scene>,
325
+ interval: f32,
326
+ loops: i64,
327
+ width: f32,
328
+ height: f32,
329
+ py: Python,
330
+ ) -> Self {
331
+ let env_type = detect_runtime_env(py).unwrap();
332
+ let rust_frames: Vec<_Scene> = frames.iter().map(|frame| frame.inner.clone()).collect();
333
+
334
+ match env_type {
335
+ RuntimeEnv::Colab | RuntimeEnv::Jupyter => {
336
+ setup_wasm_if_needed(py);
337
+ let wasm_viewer = WasmViewer::initate_viewer_and_play(py, rust_frames, (interval * 1000.0) as u64, loops, width, height);
338
+
339
+ Viewer {
340
+ environment: env_type,
341
+ wasm_viewer: Some(wasm_viewer),
342
+ native_gui_viewer: None,
343
+ }
344
+ }
345
+
346
+ RuntimeEnv::PlainScript | RuntimeEnv::IPythonTerminal => {
347
+ NativeGuiViewer::play(rust_frames, interval, loops, width, height);
348
+
349
+ Viewer {
350
+ environment: env_type,
351
+ wasm_viewer: None,
352
+ native_gui_viewer: None,
353
+ }
354
+ }
355
+ _ => panic!("Error: Invalid runtime environment"),
356
+ }
357
+ }
358
+
310
359
  /// Update the viewer with a new scene.
311
360
  ///
312
361
  /// Works for both Web-based rendering (Jupyter/Colab) and native GUI windows.
@@ -362,11 +411,13 @@ display(HTML("<div style='color:red;font-weight:bold;'>⚠️ Note: When running
362
411
  // let image = self.wasm_viewer.as_ref().unwrap().take_screenshot(py);
363
412
  print_to_notebook(
364
413
  c_str!(
365
- r#"<div style='color:red;font-weight:bold;'>⚠️ Image saving in Jupyter/Colab is not yet fully supported. This feature is still under development.</div>"))"#
414
+ r#"<div style='color:red;font-weight:bold;font-size:1rem;'>⚠️ Image saving in Jupyter/Colab is not yet fully supported.</div>"))"#
366
415
  ),
367
416
  py,
368
417
  );
369
- panic!("Error saving image. Saving images from Jupyter/Colab is not yet supported. This feature is still under development.")
418
+ panic!(
419
+ "Error saving image. Saving images from Jupyter/Colab is not yet supported."
420
+ )
370
421
  }
371
422
  RuntimeEnv::PlainScript | RuntimeEnv::IPythonTerminal => {
372
423
  let native_gui_viewer = &self.native_gui_viewer.as_ref().unwrap();
@@ -394,3 +445,9 @@ fn cosmol_viewer(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
394
445
  m.add_function(wrap_pyfunction!(parse_sdf, m)?)?;
395
446
  Ok(())
396
447
  }
448
+
449
+ fn a(py: Python, frame: Py<Scene>) -> usize {
450
+ let sc: Scene = frame.extract(py).unwrap();
451
+
452
+ 1
453
+ }
@@ -101,7 +101,7 @@ impl PySphere {
101
101
  ///
102
102
  /// # Examples
103
103
  /// ```python
104
- /// from cosmol import Scene, Stick, Viewer
104
+ /// from cosmol_viewer import Scene, Stick, Viewer
105
105
  ///
106
106
  /// stick = Stick([0.0, 0.0, 0.0], [1.0, 1.0, 1.0], 0.05)
107
107
  /// stick = stick.color([0.5, 0.8, 1.0]).opacity(0.7)
@@ -132,7 +132,7 @@ impl PyStick {
132
132
  /// # Returns
133
133
  /// `Stick`: The updated stick object.
134
134
  ///
135
- /// # Example
135
+ /// # Examples
136
136
  /// ```python
137
137
  /// stick.color([1.0, 0.0, 0.0])
138
138
  /// ```
@@ -15,7 +15,7 @@ eframe = { workspace = true, optional = true }
15
15
  wasm-bindgen-futures = { version = "0.4.50", optional = true }
16
16
  web-sys = { version = "0.3.77", features = ["HtmlCanvasElement"], optional = true }
17
17
  wasm-bindgen = { version = "0.2.100", optional = true }
18
-
18
+ gloo-timers = { version = "0.3.0", features = ["futures-core", "futures"], optional = true }
19
19
  pyo3 = { workspace = true, optional = true }
20
20
  base64 = { version = "0.22.1", optional = true }
21
21
 
@@ -26,7 +26,8 @@ wasm = [
26
26
  "eframe",
27
27
  "wasm-bindgen-futures",
28
28
  "web-sys",
29
- "wasm-bindgen"
29
+ "wasm-bindgen",
30
+ "gloo-timers"
30
31
  ]
31
32
 
32
33
  js_bridge = [
@@ -0,0 +1,420 @@
1
+ use cosmol_viewer_core::App;
2
+ use cosmol_viewer_core::scene::Scene;
3
+ #[cfg(feature = "js_bridge")]
4
+ use serde::Serialize;
5
+ use std::sync::Arc;
6
+ use std::sync::Mutex;
7
+
8
+ const VERSION: &str = env!("CARGO_PKG_VERSION"); // crate 当前版本号
9
+
10
+ #[cfg(feature = "wasm")]
11
+ use web_sys::HtmlCanvasElement;
12
+
13
+ #[cfg(feature = "wasm")]
14
+ use wasm_bindgen::JsValue;
15
+ #[cfg(feature = "wasm")]
16
+ use wasm_bindgen::prelude::wasm_bindgen;
17
+
18
+ #[cfg(feature = "js_bridge")]
19
+ use pyo3::Python;
20
+ #[cfg(feature = "js_bridge")]
21
+ pub fn setup_wasm_if_needed(py: Python) {
22
+ use base64::Engine;
23
+ use pyo3::types::PyAnyMethods;
24
+
25
+ const JS_CODE: &str = include_str!("../../wasm/pkg/cosmol_viewer_wasm.js");
26
+ const WASM_BYTES: &[u8] = include_bytes!("../../wasm/pkg/cosmol_viewer_wasm_bg.wasm");
27
+
28
+ let js_base64 = base64::engine::general_purpose::STANDARD.encode(JS_CODE);
29
+ let wasm_base64 = base64::engine::general_purpose::STANDARD.encode(WASM_BYTES);
30
+
31
+ let combined_js = format!(
32
+ r#"
33
+ (function() {{
34
+ const version = "{VERSION}";
35
+ const ns = "cosmol_viewer_" + version;
36
+
37
+ if (!window[ns + "_ready"]) {{
38
+ // 1. setup JS module
39
+ const jsCode = atob("{js_base64}");
40
+ const jsBlob = new Blob([jsCode], {{ type: 'application/javascript' }});
41
+ window[ns + "_blob_url"] = URL.createObjectURL(jsBlob);
42
+
43
+ // 2. preload WASM
44
+ const wasmBytes = Uint8Array.from(atob("{wasm_base64}"), c => c.charCodeAt(0));
45
+ window[ns + "_wasm_bytes"] = wasmBytes;
46
+
47
+ window[ns + "_ready"] = true;
48
+ console.log("Cosmol viewer setup done, version:", version);
49
+ }}
50
+ }})();
51
+ "#,
52
+ VERSION = VERSION,
53
+ js_base64 = js_base64,
54
+ wasm_base64 = wasm_base64
55
+ );
56
+
57
+ let ipython = py.import("IPython.display").unwrap();
58
+ let display = ipython.getattr("display").unwrap();
59
+
60
+ let js = ipython
61
+ .getattr("Javascript")
62
+ .unwrap()
63
+ .call1((combined_js,))
64
+ .unwrap();
65
+ display.call1((js,)).unwrap();
66
+ }
67
+
68
+ #[cfg(feature = "js_bridge")]
69
+ pub struct WasmViewer {
70
+ pub id: String,
71
+ }
72
+ #[cfg(feature = "js_bridge")]
73
+ impl WasmViewer {
74
+ pub fn initate_viewer(py: Python, scene: &Scene, width: f32, height: f32) -> Self {
75
+ use pyo3::types::PyAnyMethods;
76
+ use uuid::Uuid;
77
+
78
+ let unique_id = format!("cosmol_viewer_{}", Uuid::new_v4());
79
+
80
+ let html_code = format!(
81
+ r#"
82
+ <canvas id="{id}" width="{width}" height="{height}" style="width:{width}px; height:{height}px;"></canvas>
83
+ "#,
84
+ id = unique_id,
85
+ width = width,
86
+ height = height
87
+ );
88
+
89
+ let scene_json = serde_json::to_string(scene).unwrap();
90
+ let escaped = serde_json::to_string(&scene_json).unwrap();
91
+
92
+ let combined_js = format!(
93
+ r#"
94
+ (function() {{
95
+ const version = "{VERSION}";
96
+ const ns = "cosmol_viewer_" + version;
97
+
98
+ import(window[ns + "_blob_url"]).then(async (mod) => {{
99
+ await mod.default(window[ns + "_wasm_bytes"]);
100
+
101
+ const canvas = document.getElementById('{id}');
102
+ const app = new mod.WebHandle();
103
+ const sceneJson = {SCENE_JSON};
104
+ await app.start_with_scene(canvas, sceneJson);
105
+
106
+ window[ns + "_instances"] = window[ns + "_instances"] || {{}};
107
+ window[ns + "_instances"]["{id}"] = app;
108
+ console.log("Cosmol viewer instance {id} (v{VERSION}) started");
109
+ }});
110
+ }})();
111
+ "#,
112
+ VERSION = VERSION,
113
+ id = unique_id,
114
+ SCENE_JSON = escaped
115
+ );
116
+ let ipython = py.import("IPython.display").unwrap();
117
+ let display = ipython.getattr("display").unwrap();
118
+
119
+ let html = ipython
120
+ .getattr("HTML")
121
+ .unwrap()
122
+ .call1((html_code,))
123
+ .unwrap();
124
+ display.call1((html,)).unwrap();
125
+
126
+ let js = ipython
127
+ .getattr("Javascript")
128
+ .unwrap()
129
+ .call1((combined_js,))
130
+ .unwrap();
131
+ display.call1((js,)).unwrap();
132
+
133
+ Self { id: unique_id }
134
+ }
135
+
136
+ pub fn initate_viewer_and_play(
137
+ py: Python,
138
+ frames: Vec<Scene>,
139
+ interval: u64,
140
+ loops: i64,
141
+ width: f32,
142
+ height: f32,
143
+ ) -> Self {
144
+ use pyo3::types::PyAnyMethods;
145
+ use uuid::Uuid;
146
+
147
+ let unique_id = format!("cosmol_viewer_{}", Uuid::new_v4());
148
+
149
+ let html_code = format!(
150
+ r#"
151
+ <canvas id="{id}" width="{width}" height="{height}" style="width:{width}px; height:{height}px;"></canvas>
152
+ "#,
153
+ id = unique_id,
154
+ width = width,
155
+ height = height
156
+ );
157
+
158
+ let frames_json = serde_json::to_string(&Frames {
159
+ frames,
160
+ interval,
161
+ loops,
162
+ })
163
+ .unwrap();
164
+ let escaped = serde_json::to_string(&frames_json).unwrap();
165
+
166
+ let combined_js = format!(
167
+ r#"
168
+ (function() {{
169
+ const version = "{VERSION}";
170
+ const ns = "cosmol_viewer_" + version;
171
+
172
+ import(window[ns + "_blob_url"]).then(async (mod) => {{
173
+ await mod.default(window[ns + "_wasm_bytes"]);
174
+
175
+ const canvas = document.getElementById('{id}');
176
+ const app = new mod.WebHandle();
177
+ const framesJson = {FRAMES_JSON};
178
+ await app.initate_viewer_and_play(canvas, framesJson);
179
+
180
+ window[ns + "_instances"] = window[ns + "_instances"] || {{}};
181
+ window[ns + "_instances"]["{id}"] = app;
182
+ console.log("Cosmol viewer instance {id} (v{VERSION}) started");
183
+ }});
184
+ }})();
185
+ "#,
186
+ VERSION = VERSION,
187
+ id = unique_id,
188
+ FRAMES_JSON = escaped
189
+ );
190
+ let ipython = py.import("IPython.display").unwrap();
191
+ let display = ipython.getattr("display").unwrap();
192
+
193
+ let html = ipython
194
+ .getattr("HTML")
195
+ .unwrap()
196
+ .call1((html_code,))
197
+ .unwrap();
198
+ display.call1((html,)).unwrap();
199
+
200
+ let js = ipython
201
+ .getattr("Javascript")
202
+ .unwrap()
203
+ .call1((combined_js,))
204
+ .unwrap();
205
+ display.call1((js,)).unwrap();
206
+
207
+ Self { id: unique_id }
208
+ }
209
+
210
+ pub fn call<T: Serialize>(&self, py: Python, name: &str, input: T) -> () {
211
+ use pyo3::types::PyAnyMethods;
212
+
213
+ let input_json = serde_json::to_string(&input).unwrap();
214
+ let escaped = serde_json::to_string(&input_json).unwrap();
215
+ let combined_js = format!(
216
+ r#"
217
+ (async function() {{
218
+ const ns = "cosmol_viewer_" + "{VERSION}";
219
+ const instances = window[ns + "_instances"] || {{}};
220
+ const app = instances["{id}"];
221
+ if (app) {{
222
+ try {{
223
+ const result = await app.{name}({escaped});
224
+ console.log("Call `{name}` on instance {id} (v{VERSION}) result:", result);
225
+ }} catch (err) {{
226
+ console.error("Error calling `{name}` on instance {id} (v{VERSION}):", err);
227
+ }}
228
+ }} else {{
229
+ console.error("No app found for ID {id} in namespace", ns);
230
+ }}
231
+ }})();
232
+ "#,
233
+ VERSION = VERSION,
234
+ id = self.id,
235
+ name = name,
236
+ escaped = escaped
237
+ );
238
+
239
+ let ipython = py.import("IPython.display").unwrap();
240
+ let display = ipython.getattr("display").unwrap();
241
+
242
+ let js = ipython
243
+ .getattr("Javascript")
244
+ .unwrap()
245
+ .call1((combined_js,))
246
+ .unwrap();
247
+ let _ = display.call1((js,));
248
+ }
249
+
250
+ pub fn update(&self, py: Python, scene: &Scene) {
251
+ self.call(py, "update_scene", scene);
252
+ }
253
+
254
+ pub fn take_screenshot(&self, py: Python) {
255
+ self.call(py, "take_screenshot", None::<u8>)
256
+ }
257
+ }
258
+
259
+ #[derive(serde::Serialize, serde::Deserialize)]
260
+ struct Frames {
261
+ frames: Vec<Scene>,
262
+ interval: u64,
263
+ loops: i64, // -1 = infinite
264
+ }
265
+
266
+ pub trait JsBridge {
267
+ fn update(scene: &Scene) -> ();
268
+ }
269
+
270
+ #[cfg(feature = "wasm")]
271
+ #[cfg(target_arch = "wasm32")]
272
+ use eframe::WebRunner;
273
+
274
+ #[cfg(feature = "wasm")]
275
+ #[cfg(not(target_arch = "wasm32"))]
276
+ struct WebRunner;
277
+
278
+ #[cfg(feature = "wasm")]
279
+ #[cfg(not(target_arch = "wasm32"))]
280
+ impl WebRunner {
281
+ pub fn new() -> Self {
282
+ Self
283
+ }
284
+ }
285
+
286
+ #[cfg(feature = "wasm")]
287
+ #[wasm_bindgen]
288
+ pub struct WebHandle {
289
+ runner: WebRunner,
290
+ app: Arc<Mutex<Option<App>>>,
291
+ }
292
+
293
+ #[cfg(feature = "wasm")]
294
+ #[wasm_bindgen]
295
+ impl WebHandle {
296
+ #[wasm_bindgen(constructor)]
297
+ pub fn new() -> Self {
298
+ #[cfg(target_arch = "wasm32")]
299
+ eframe::WebLogger::init(log::LevelFilter::Debug).ok();
300
+ Self {
301
+ runner: WebRunner::new(),
302
+ app: Arc::new(Mutex::new(None)),
303
+ }
304
+ }
305
+
306
+ #[wasm_bindgen]
307
+ pub async fn start_with_scene(
308
+ &mut self,
309
+ canvas: HtmlCanvasElement,
310
+ scene_json: String,
311
+ ) -> Result<(), JsValue> {
312
+ let scene: Scene = serde_json::from_str(&scene_json)
313
+ .map_err(|e| JsValue::from_str(&format!("Scene parse error: {}", e)))?;
314
+
315
+ let app = Arc::clone(&self.app);
316
+
317
+ #[cfg(target_arch = "wasm32")]
318
+ let _ = self
319
+ .runner
320
+ .start(
321
+ canvas,
322
+ eframe::WebOptions::default(),
323
+ Box::new(move |cc| {
324
+ use cosmol_viewer_core::AppWrapper;
325
+
326
+ let mut guard = app.lock().unwrap();
327
+ *guard = Some(App::new(cc, scene));
328
+ Ok(Box::new(AppWrapper(app.clone())))
329
+ }),
330
+ )
331
+ .await;
332
+ Ok(())
333
+ }
334
+
335
+ #[wasm_bindgen]
336
+ pub async fn update_scene(&mut self, scene_json: String) -> Result<(), JsValue> {
337
+ let scene: Scene = serde_json::from_str(&scene_json)
338
+ .map_err(|e| JsValue::from_str(&format!("Scene parse error: {}", e)))?;
339
+
340
+ let mut app_guard = self.app.lock().unwrap();
341
+ if let Some(app) = &mut *app_guard {
342
+ println!("Received scene update");
343
+ app.update_scene(scene);
344
+ app.ctx.request_repaint();
345
+ } else {
346
+ println!("scene update received but app is not initialized");
347
+ }
348
+ Ok(())
349
+ }
350
+
351
+ #[wasm_bindgen]
352
+ pub async fn initate_viewer_and_play(
353
+ &mut self,
354
+ canvas: HtmlCanvasElement,
355
+ frames_json: String,
356
+ ) -> Result<(), JsValue> {
357
+ use std::{thread, time::Duration};
358
+ let frames: Frames = serde_json::from_str(&frames_json)
359
+ .map_err(|e| JsValue::from_str(&format!("Frames parse error: {}", e)))?;
360
+
361
+ let app = Arc::clone(&self.app);
362
+
363
+ let scene = frames.frames[0].clone();
364
+
365
+ #[cfg(target_arch = "wasm32")]
366
+ let _ = self
367
+ .runner
368
+ .start(
369
+ canvas,
370
+ eframe::WebOptions::default(),
371
+ Box::new(move |cc| {
372
+ use cosmol_viewer_core::AppWrapper;
373
+
374
+ let mut guard = app.lock().unwrap();
375
+ *guard = Some(App::new(cc, scene));
376
+ Ok(Box::new(AppWrapper(app.clone())))
377
+ }),
378
+ )
379
+ .await;
380
+
381
+ let timeout_ms = 30000;
382
+ let mut waited = 0;
383
+ loop {
384
+ if self.app.lock().unwrap().is_some() {
385
+ break;
386
+ }
387
+ if waited > timeout_ms {
388
+ panic!("Fail to initialize App");
389
+ }
390
+ gloo_timers::future::sleep(Duration::from_millis(10)).await;
391
+ // Delay::new(Duration::from_secs(1)).await.unwrap();
392
+ waited += 10;
393
+ }
394
+
395
+ let mut count = 0;
396
+ loop {
397
+ if frames.loops >= 0 && count >= frames.loops {
398
+ break;
399
+ }
400
+ count += 1;
401
+ for frame in &frames.frames {
402
+ {
403
+ let mut guard = self.app.lock().unwrap();
404
+ if let Some(app) = &mut *guard {
405
+ app.update_scene(frame.clone());
406
+ app.ctx.request_repaint();
407
+ }
408
+ }
409
+ gloo_timers::future::sleep(Duration::from_millis(frames.interval)).await;
410
+ }
411
+ }
412
+
413
+ Ok(())
414
+ }
415
+
416
+ #[wasm_bindgen]
417
+ pub async fn take_screenshot(&self) -> Option<String> {
418
+ Some("The returned value is omitted!".to_string())
419
+ }
420
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cosmol-viewer"
3
- version = "0.1.2.dev5"
3
+ version = "0.1.3.dev1"
4
4
  description = "Molecular visualization tools"
5
5
  authors = [{name = "95028", email = "wjt@cosmol.org"}]
6
6
 
@@ -1,70 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: cosmol-viewer
3
- Version: 0.1.2.dev5
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
- <div align="center">
12
- <a href="https://crates.io/crates/cosmol_viewer">
13
- <img src="https://img.shields.io/crates/v/cosmol_viewer.svg" alt="crates.io Latest Release"/>
14
- </a>
15
- <a href="https://pypi.org/project/cosmol_viewer/">
16
- <img src="https://img.shields.io/pypi/v/cosmol_viewer.svg" alt="PyPi Latest Release"/>
17
- </a>
18
- </div>
19
-
20
- A high-performance molecular visualization library built with Rust and WebGPU, designed for seamless integration into Python workflows.
21
-
22
- - ⚡ Fast: Native-speed rendering powered by Rust and GPU acceleration
23
-
24
- - 🧬 Flexible: Load molecules from .sdf, .pdb, and dynamically update 3D structures
25
-
26
- - 📓 Notebook-friendly: Fully supports Jupyter and Google Colab — ideal for education, research, and live demos
27
-
28
- - 🔁 Real-time updates: Update molecular coordinates on-the-fly for simulations or animations
29
-
30
- - 🎨 Customizable: Control styles, camera, and rendering settings programmatically
31
-
32
- # Installation
33
-
34
- ```sh
35
- pip install cosmol-viewer
36
- ```
37
-
38
- # Examples
39
-
40
- See examples in [Google Colab](https://colab.research.google.com/drive/1Sw72QWjQh_sbbY43jGyBOfF1AQCycmIx?usp=sharing).
41
-
42
- # Usage
43
-
44
- ```python
45
- from cosmol_viewer import Scene, Viewer, parse_sdf, Molecules
46
-
47
- # === Step 1: Load and render a molecule ===
48
- with open("molecule.sdf", "r") as f:
49
- sdf = f.read()
50
- mol = Molecules(parse_sdf(sdf)).centered()
51
-
52
- scene = Scene()
53
- scene.scale(0.1)
54
- scene.add_shape(mol, "mol")
55
-
56
- viewer = Viewer.render(scene) # Launch the viewer
57
-
58
- # === Step 2: Update the same molecule dynamically ===
59
- import time
60
-
61
- for i in range(1, 10): # Simulate multiple frames
62
- with open(f"frames/frame_{i}.sdf", "r") as f:
63
- sdf = f.read()
64
- updated_mol = Molecules(parse_sdf(sdf)).centered()
65
-
66
- scene.update_shape("mol", updated_mol)
67
- viewer.update(scene)
68
-
69
- time.sleep(0.033) # ~30 FPS
70
- ```
@@ -1,257 +0,0 @@
1
- use cosmol_viewer_core::App;
2
- use cosmol_viewer_core::scene::Scene;
3
- #[cfg(feature = "js_bridge")]
4
- use serde::Serialize;
5
- use std::sync::Arc;
6
- use std::sync::Mutex;
7
-
8
- #[cfg(feature = "wasm")]
9
- use web_sys::HtmlCanvasElement;
10
-
11
- #[cfg(feature = "wasm")]
12
- use wasm_bindgen::prelude::wasm_bindgen;
13
- #[cfg(feature = "wasm")]
14
- use wasm_bindgen::JsValue;
15
-
16
- #[cfg(feature = "js_bridge")]
17
- use pyo3::Python;
18
- #[cfg(feature = "js_bridge")]
19
- pub fn setup_wasm_if_needed(py: Python) {
20
- use base64::Engine;
21
- use pyo3::types::PyAnyMethods;
22
-
23
- const JS_CODE: &str = include_str!("../../wasm/pkg/cosmol_viewer_wasm.js");
24
-
25
- let js_base64 = base64::engine::general_purpose::STANDARD.encode(JS_CODE);
26
-
27
- let combined_js = format!(
28
- r#"
29
- (function() {{
30
- if (!window.cosmol_viewer_blob_url) {{
31
- const jsCode = atob("{js_base64}");
32
- const blob = new Blob([jsCode], {{ type: 'application/javascript' }});
33
- window.cosmol_viewer_blob_url = URL.createObjectURL(blob);
34
- }}
35
- }})();
36
- "#
37
- );
38
-
39
- let ipython = py.import("IPython.display").unwrap();
40
- let display = ipython.getattr("display").unwrap();
41
-
42
- let js = ipython
43
- .getattr("Javascript")
44
- .unwrap()
45
- .call1((combined_js,))
46
- .unwrap();
47
- display.call1((js,)).unwrap();
48
- }
49
-
50
- #[cfg(feature = "js_bridge")]
51
- pub struct WasmViewer {
52
- pub id: String,
53
- }
54
- #[cfg(feature = "js_bridge")]
55
- impl WasmViewer {
56
- pub fn initate_viewer(py: Python, scene: &Scene) -> Self {
57
- use base64::Engine;
58
- use pyo3::types::PyAnyMethods;
59
- use uuid::Uuid;
60
-
61
- let unique_id = format!("cosmol_viewer_{}", Uuid::new_v4());
62
- const WASM_BYTES: &[u8] = include_bytes!("../../wasm/pkg/cosmol_viewer_wasm_bg.wasm");
63
- let wasm_base64 = base64::engine::general_purpose::STANDARD.encode(WASM_BYTES);
64
-
65
- let viewport_size = scene.viewport.unwrap_or([800, 500]);
66
-
67
- let html_code = format!(
68
- r#"
69
- <canvas id="{id}" width="{width}" height="{height}" style="width:{width}px; height:{height}px;"></canvas>
70
- "#,
71
- id = unique_id,
72
- width = viewport_size[0],
73
- height = viewport_size[1]
74
- );
75
-
76
- let scene_json = serde_json::to_string(scene).unwrap();
77
- let escaped = serde_json::to_string(&scene_json).unwrap();
78
-
79
- let combined_js = format!(
80
- r#"
81
- (function() {{
82
- const wasmBase64 = "{wasm_base64}";
83
- import(window.cosmol_viewer_blob_url).then(async (mod) => {{
84
- const wasmBytes = Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0));
85
- await mod.default(wasmBytes);
86
-
87
- const canvas = document.getElementById('{id}');
88
- const app = new mod.WebHandle();
89
- const sceneJson = {SCENE_JSON};
90
- console.log("Starting cosmol_viewer with scene:", sceneJson);
91
- await app.start_with_scene(canvas, sceneJson);
92
-
93
- window.cosmol_viewer_instances = window.cosmol_viewer_instances || {{}};
94
- window.cosmol_viewer_instances["{id}"] = app;
95
- }});
96
- }})();
97
- "#,
98
- wasm_base64 = wasm_base64,
99
- id = unique_id,
100
- SCENE_JSON = escaped
101
- );
102
- let ipython = py.import("IPython.display").unwrap();
103
- let display = ipython.getattr("display").unwrap();
104
-
105
- let html = ipython
106
- .getattr("HTML")
107
- .unwrap()
108
- .call1((html_code,))
109
- .unwrap();
110
- display.call1((html,)).unwrap();
111
-
112
- let js = ipython
113
- .getattr("Javascript")
114
- .unwrap()
115
- .call1((combined_js,))
116
- .unwrap();
117
- display.call1((js,)).unwrap();
118
-
119
- Self { id: unique_id }
120
- }
121
-
122
- pub fn call<T: Serialize>(&self, py: Python, name: &str, input: T) -> () {
123
- use pyo3::types::PyAnyMethods;
124
-
125
- let input_json = serde_json::to_string(&input).unwrap();
126
- let escaped = serde_json::to_string(&input_json).unwrap();
127
- let combined_js = format!(
128
- r#"
129
- (async function() {{
130
- console.log(window.cosmol_viewer_instances)
131
- const instances = window.cosmol_viewer_instances || {{}};
132
- const app = instances["{id}"];
133
- if (app) {{
134
- const result = await app.{name}({escaped});
135
- console.log("Result:", result);
136
- console.log("Result:", app);
137
- window.cosmol_viewer_result.set("cosmol_result", result);
138
- console.log("Result:", app);
139
- }} else {{
140
- console.error("No app found for ID {id}");
141
- }}
142
- }})();
143
- "#,
144
- id = self.id,
145
- );
146
-
147
- let ipython = py.import("IPython.display").unwrap();
148
- let display = ipython.getattr("display").unwrap();
149
-
150
- let js = ipython
151
- .getattr("Javascript")
152
- .unwrap()
153
- .call1((combined_js,))
154
- .unwrap();
155
- let _ = display.call1((js,));
156
- }
157
-
158
- pub fn update(&self, py: Python, scene: &Scene) {
159
- self.call(py, "update_scene", scene);
160
- }
161
-
162
- pub fn take_screenshot(&self, py: Python) {
163
- self.call(py, "take_screenshot", None::<u8>)
164
- }
165
- }
166
-
167
- pub trait JsBridge {
168
- fn update(scene: &Scene) -> ();
169
- }
170
-
171
- #[cfg(feature = "wasm")]
172
- #[cfg(target_arch = "wasm32")]
173
- use eframe::WebRunner;
174
-
175
- #[cfg(feature = "wasm")]
176
- #[cfg(not(target_arch = "wasm32"))]
177
- struct WebRunner;
178
-
179
- #[cfg(feature = "wasm")]
180
- #[cfg(not(target_arch = "wasm32"))]
181
- impl WebRunner {
182
- pub fn new() -> Self {
183
- Self
184
- }
185
- }
186
-
187
- #[cfg(feature = "wasm")]
188
- #[wasm_bindgen]
189
- pub struct WebHandle {
190
- runner: WebRunner,
191
- app: Arc<Mutex<Option<App>>>,
192
- }
193
-
194
- #[cfg(feature = "wasm")]
195
- #[wasm_bindgen]
196
- impl WebHandle {
197
-
198
- #[wasm_bindgen(constructor)]
199
- pub fn new() -> Self {
200
- #[cfg(target_arch = "wasm32")]
201
- eframe::WebLogger::init(log::LevelFilter::Debug).ok();
202
- Self {
203
- runner: WebRunner::new(),
204
- app: Arc::new(Mutex::new(None)),
205
- }
206
- }
207
-
208
- #[wasm_bindgen]
209
- pub async fn start_with_scene(
210
- &mut self,
211
- canvas: HtmlCanvasElement,
212
- scene_json: String,
213
- ) -> Result<(), JsValue> {
214
- let scene: Scene = serde_json::from_str(&scene_json)
215
- .map_err(|e| JsValue::from_str(&format!("Scene parse error: {}", e)))?;
216
-
217
- let app = Arc::clone(&self.app);
218
-
219
- #[cfg(target_arch = "wasm32")]
220
- let _ = self
221
- .runner
222
- .start(
223
- canvas,
224
- eframe::WebOptions::default(),
225
- Box::new(move |cc| {
226
- use cosmol_viewer_core::AppWrapper;
227
-
228
- let mut guard = app.lock().unwrap();
229
- *guard = Some(App::new(cc, scene));
230
- Ok(Box::new(AppWrapper(app.clone())))
231
- }),
232
- )
233
- .await;
234
- Ok(())
235
- }
236
-
237
- #[wasm_bindgen]
238
- pub async fn update_scene(&mut self, scene_json: String) -> Result<(), JsValue> {
239
- let scene: Scene = serde_json::from_str(&scene_json)
240
- .map_err(|e| JsValue::from_str(&format!("Scene parse error: {}", e)))?;
241
-
242
- let mut app_guard = self.app.lock().unwrap();
243
- if let Some(app) = &mut *app_guard {
244
- println!("Received scene update");
245
- app.update_scene(scene);
246
- app.ctx.request_repaint();
247
- } else {
248
- println!("scene update received but app is not initialized");
249
- }
250
- Ok(())
251
- }
252
-
253
- #[wasm_bindgen]
254
- pub async fn take_screenshot(&self) -> Option<String> {
255
- Some("javavavavavavav".to_string())
256
- }
257
- }