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.
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/Cargo.lock +25 -3
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/Cargo.toml +3 -3
- cosmol_viewer-0.1.3.dev1/PKG-INFO +6 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/lib.rs +117 -5
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/scene.rs +3 -3
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/Cargo.toml +1 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/README.md +3 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/src/lib.rs +81 -24
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/src/shapes.rs +2 -2
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/wasm/Cargo.toml +3 -2
- cosmol_viewer-0.1.3.dev1/crates/wasm/src/lib.rs +420 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/pyproject.toml +1 -1
- cosmol_viewer-0.1.2.dev5/PKG-INFO +0 -70
- cosmol_viewer-0.1.2.dev5/crates/wasm/src/lib.rs +0 -257
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/Cargo.toml +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/parser/mod.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/parser/sdf.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/bg_fragment.glsl +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/bg_vertex.glsl +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/canvas.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/fragment.glsl +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/mod.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/vertex.glsl +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shapes/mod.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shapes/molecules.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shapes/sphere.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shapes/stick.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/utils.rs +0 -0
- {cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/python/build.rs +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
17
|
-
cosmol_viewer_core = { version = "0.1.
|
|
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"] }
|
|
@@ -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 =
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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!(
|
|
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
|
|
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
|
-
/// #
|
|
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,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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cosmol_viewer-0.1.2.dev5 → cosmol_viewer-0.1.3.dev1}/crates/core/src/shader/bg_fragment.glsl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|