streamlit-pyvista 0.0.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.
- streamlit_pyvista-0.0.1/PKG-INFO +89 -0
- streamlit_pyvista-0.0.1/README.md +4 -0
- streamlit_pyvista-0.0.1/pyproject.toml +93 -0
- streamlit_pyvista-0.0.1/streamlit_pyvista/MeshViewerComponent.py +83 -0
- streamlit_pyvista-0.0.1/streamlit_pyvista/__init__.py +2 -0
- streamlit_pyvista-0.0.1/streamlit_pyvista/trame_viewer.py +479 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: streamlit-pyvista
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Maxime Rochat
|
|
6
|
+
Author-email: rochat.max@gmail.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: aiohttp (==3.9.3)
|
|
13
|
+
Requires-Dist: aiosignal (==1.3.1)
|
|
14
|
+
Requires-Dist: altair (==5.2.0)
|
|
15
|
+
Requires-Dist: async-timeout (==4.0.3)
|
|
16
|
+
Requires-Dist: attrs (==23.2.0)
|
|
17
|
+
Requires-Dist: blinker (==1.7.0)
|
|
18
|
+
Requires-Dist: cachetools (==5.3.2)
|
|
19
|
+
Requires-Dist: certifi (==2024.2.2)
|
|
20
|
+
Requires-Dist: charset-normalizer (==3.3.2)
|
|
21
|
+
Requires-Dist: click (==8.1.7)
|
|
22
|
+
Requires-Dist: contourpy (==1.2.0)
|
|
23
|
+
Requires-Dist: cycler (==0.12.1)
|
|
24
|
+
Requires-Dist: fonttools (==4.49.0)
|
|
25
|
+
Requires-Dist: frozenlist (==1.4.1)
|
|
26
|
+
Requires-Dist: gitdb (==4.0.11)
|
|
27
|
+
Requires-Dist: gitpython (==3.1.42)
|
|
28
|
+
Requires-Dist: idna (==3.6)
|
|
29
|
+
Requires-Dist: importlib-metadata (==7.0.1)
|
|
30
|
+
Requires-Dist: jinja2 (==3.1.3)
|
|
31
|
+
Requires-Dist: jsonschema (==4.21.1)
|
|
32
|
+
Requires-Dist: jsonschema-specifications (==2023.12.1)
|
|
33
|
+
Requires-Dist: kiwisolver (==1.4.5)
|
|
34
|
+
Requires-Dist: markdown-it-py (==3.0.0)
|
|
35
|
+
Requires-Dist: markupsafe (==2.1.5)
|
|
36
|
+
Requires-Dist: matplotlib (==3.8.3)
|
|
37
|
+
Requires-Dist: mdurl (==0.1.2)
|
|
38
|
+
Requires-Dist: more-itertools (==10.2.0)
|
|
39
|
+
Requires-Dist: multidict (==6.0.5)
|
|
40
|
+
Requires-Dist: numpy (==1.26.4)
|
|
41
|
+
Requires-Dist: packaging (==23.2)
|
|
42
|
+
Requires-Dist: pandas (==2.2.0)
|
|
43
|
+
Requires-Dist: pillow (==10.2.0)
|
|
44
|
+
Requires-Dist: platformdirs (==4.2.0)
|
|
45
|
+
Requires-Dist: pooch (==1.8.1)
|
|
46
|
+
Requires-Dist: protobuf (==4.25.3)
|
|
47
|
+
Requires-Dist: pyarrow (==15.0.0)
|
|
48
|
+
Requires-Dist: pydeck (==0.8.1b0)
|
|
49
|
+
Requires-Dist: pygments (==2.17.2)
|
|
50
|
+
Requires-Dist: pyparsing (==3.1.1)
|
|
51
|
+
Requires-Dist: pytest (>=8.1.1,<9.0.0)
|
|
52
|
+
Requires-Dist: python-dateutil (==2.8.2)
|
|
53
|
+
Requires-Dist: pytz (==2024.1)
|
|
54
|
+
Requires-Dist: pyvista (==0.43.3)
|
|
55
|
+
Requires-Dist: referencing (==0.33.0)
|
|
56
|
+
Requires-Dist: requests (==2.31.0)
|
|
57
|
+
Requires-Dist: rich (==13.7.0)
|
|
58
|
+
Requires-Dist: rpds-py (==0.18.0)
|
|
59
|
+
Requires-Dist: scooby (==0.9.2)
|
|
60
|
+
Requires-Dist: six (==1.16.0)
|
|
61
|
+
Requires-Dist: smmap (==5.0.1)
|
|
62
|
+
Requires-Dist: streamlit (==1.31.1)
|
|
63
|
+
Requires-Dist: tenacity (==8.2.3)
|
|
64
|
+
Requires-Dist: toml (==0.10.2)
|
|
65
|
+
Requires-Dist: toolz (==0.12.1)
|
|
66
|
+
Requires-Dist: tornado (==6.4)
|
|
67
|
+
Requires-Dist: trame (==3.5.2)
|
|
68
|
+
Requires-Dist: trame-client (==2.16.1)
|
|
69
|
+
Requires-Dist: trame-plotly (==3.0.2)
|
|
70
|
+
Requires-Dist: trame-server (==2.17.2)
|
|
71
|
+
Requires-Dist: trame-vtk (==2.8.5)
|
|
72
|
+
Requires-Dist: trame-vuetify (==2.4.2)
|
|
73
|
+
Requires-Dist: typing-extensions (==4.9.0)
|
|
74
|
+
Requires-Dist: tzdata (==2024.1)
|
|
75
|
+
Requires-Dist: tzlocal (==5.2)
|
|
76
|
+
Requires-Dist: urllib3 (==2.2.1)
|
|
77
|
+
Requires-Dist: validators (==0.22.0)
|
|
78
|
+
Requires-Dist: vtk (==9.3.0)
|
|
79
|
+
Requires-Dist: watchdog (==4.0.0)
|
|
80
|
+
Requires-Dist: wslink (==1.12.4)
|
|
81
|
+
Requires-Dist: yarl (==1.9.4)
|
|
82
|
+
Requires-Dist: zipp (==3.17.0)
|
|
83
|
+
Description-Content-Type: text/markdown
|
|
84
|
+
|
|
85
|
+
# streamlit-pyvista
|
|
86
|
+
A Streamlit component that allow support for new pyvista viewer backend : Trame.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "streamlit-pyvista"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["Maxime Rochat <rochat.max@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.10"
|
|
10
|
+
aiohttp = "3.9.3"
|
|
11
|
+
aiosignal = "1.3.1"
|
|
12
|
+
altair = "5.2.0"
|
|
13
|
+
async-timeout = "4.0.3"
|
|
14
|
+
attrs = "23.2.0"
|
|
15
|
+
blinker = "1.7.0"
|
|
16
|
+
cachetools = "5.3.2"
|
|
17
|
+
certifi = "2024.2.2"
|
|
18
|
+
charset-normalizer = "3.3.2"
|
|
19
|
+
click = "8.1.7"
|
|
20
|
+
contourpy = "1.2.0"
|
|
21
|
+
cycler = "0.12.1"
|
|
22
|
+
fonttools = "4.49.0"
|
|
23
|
+
frozenlist = "1.4.1"
|
|
24
|
+
gitdb = "4.0.11"
|
|
25
|
+
gitpython = "3.1.42"
|
|
26
|
+
idna = "3.6"
|
|
27
|
+
importlib-metadata = "7.0.1"
|
|
28
|
+
jinja2 = "3.1.3"
|
|
29
|
+
jsonschema = "4.21.1"
|
|
30
|
+
jsonschema-specifications = "2023.12.1"
|
|
31
|
+
kiwisolver = "1.4.5"
|
|
32
|
+
markdown-it-py = "3.0.0"
|
|
33
|
+
markupsafe = "2.1.5"
|
|
34
|
+
matplotlib = "3.8.3"
|
|
35
|
+
mdurl = "0.1.2"
|
|
36
|
+
more-itertools = "10.2.0"
|
|
37
|
+
multidict = "6.0.5"
|
|
38
|
+
numpy = "1.26.4"
|
|
39
|
+
packaging = "23.2"
|
|
40
|
+
pandas = "2.2.0"
|
|
41
|
+
pillow = "10.2.0"
|
|
42
|
+
platformdirs = "4.2.0"
|
|
43
|
+
pooch = "1.8.1"
|
|
44
|
+
protobuf = "4.25.3"
|
|
45
|
+
pyarrow = "15.0.0"
|
|
46
|
+
pydeck = "0.8.1b0"
|
|
47
|
+
pygments = "2.17.2"
|
|
48
|
+
pyparsing = "3.1.1"
|
|
49
|
+
python-dateutil = "2.8.2"
|
|
50
|
+
pytz = "2024.1"
|
|
51
|
+
pyvista = "0.43.3"
|
|
52
|
+
referencing = "0.33.0"
|
|
53
|
+
requests = "2.31.0"
|
|
54
|
+
rich = "13.7.0"
|
|
55
|
+
rpds-py = "0.18.0"
|
|
56
|
+
scooby = "0.9.2"
|
|
57
|
+
six = "1.16.0"
|
|
58
|
+
smmap = "5.0.1"
|
|
59
|
+
streamlit = "1.31.1"
|
|
60
|
+
tenacity = "8.2.3"
|
|
61
|
+
toml = "0.10.2"
|
|
62
|
+
toolz = "0.12.1"
|
|
63
|
+
tornado = "6.4"
|
|
64
|
+
trame = "3.5.2"
|
|
65
|
+
trame-client = "2.16.1"
|
|
66
|
+
trame-plotly = "3.0.2"
|
|
67
|
+
trame-server = "2.17.2"
|
|
68
|
+
trame-vtk = "2.8.5"
|
|
69
|
+
trame-vuetify = "2.4.2"
|
|
70
|
+
typing-extensions = "4.9.0"
|
|
71
|
+
tzdata = "2024.1"
|
|
72
|
+
tzlocal = "5.2"
|
|
73
|
+
urllib3 = "2.2.1"
|
|
74
|
+
validators = "0.22.0"
|
|
75
|
+
vtk = "9.3.0"
|
|
76
|
+
watchdog = "4.0.0"
|
|
77
|
+
wslink = "1.12.4"
|
|
78
|
+
yarl = "1.9.4"
|
|
79
|
+
zipp = "3.17.0"
|
|
80
|
+
pytest = "^8.1.1"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
[build-system]
|
|
84
|
+
requires = ["poetry-core", "poetry-dynamic-versioning"]
|
|
85
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
[tool.poetry-dynamic-versioning]
|
|
90
|
+
enable = false
|
|
91
|
+
vcs = "git"
|
|
92
|
+
style = "semver"
|
|
93
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import streamlit as st
|
|
5
|
+
import streamlit.components.v1 as components
|
|
6
|
+
import requests
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
class MeshViewerComponent:
|
|
12
|
+
|
|
13
|
+
def __init__(self, mesh_path: str = None, sequence_size=1, server_url="http://127.0.0.1:8080"):
|
|
14
|
+
if not os.path.isabs(mesh_path):
|
|
15
|
+
mesh_path = os.getcwd() + "/" + mesh_path
|
|
16
|
+
|
|
17
|
+
if sequence_size != 1:
|
|
18
|
+
self.mesh_array = []
|
|
19
|
+
for i in range(sequence_size):
|
|
20
|
+
path = mesh_path % i
|
|
21
|
+
if not os.path.exists(path):
|
|
22
|
+
raise FileNotFoundError(f"The file '{path}' does not exist.")
|
|
23
|
+
elif not os.path.exists(mesh_path):
|
|
24
|
+
raise FileNotFoundError(f"The file '{mesh_path}' does not exist.")
|
|
25
|
+
|
|
26
|
+
self.server_url = server_url
|
|
27
|
+
self.sequence_size = sequence_size
|
|
28
|
+
self.mesh_path = mesh_path
|
|
29
|
+
self.width = 800
|
|
30
|
+
self.height = 900
|
|
31
|
+
self.setup_server()
|
|
32
|
+
self.set_mesh()
|
|
33
|
+
|
|
34
|
+
def is_server_alive(self):
|
|
35
|
+
try:
|
|
36
|
+
requests.get(self.server_url)
|
|
37
|
+
return True
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def setup_server(self):
|
|
42
|
+
if self.is_server_alive():
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
print("Started new server")
|
|
47
|
+
trame_viewer_thread = threading.Thread(target=self.run_trame_viewer)
|
|
48
|
+
# Start the thread
|
|
49
|
+
trame_viewer_thread.start()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_trame_viewer(self):
|
|
53
|
+
try:
|
|
54
|
+
pass
|
|
55
|
+
subprocess.run(["python3", os.path.dirname(os.path.abspath(__file__)) + "/trame_viewer.py"], check=True) #stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
56
|
+
except subprocess.CalledProcessError as e:
|
|
57
|
+
print("Error:", e)
|
|
58
|
+
|
|
59
|
+
def wait_for_server_alive(self):
|
|
60
|
+
init_time = time.time()
|
|
61
|
+
while not self.is_server_alive():
|
|
62
|
+
if time.time() - init_time >= 3:
|
|
63
|
+
init_time = time.time()
|
|
64
|
+
self.setup_server()
|
|
65
|
+
|
|
66
|
+
def set_mesh(self):
|
|
67
|
+
self.wait_for_server_alive()
|
|
68
|
+
url = self.server_url + "/select_mesh"
|
|
69
|
+
data = {
|
|
70
|
+
"mesh_path": self.mesh_path,
|
|
71
|
+
"nbr_frames": self.sequence_size,
|
|
72
|
+
"width": self.width,
|
|
73
|
+
"height": self.height
|
|
74
|
+
}
|
|
75
|
+
print(f"MESH LOADED IS {self.mesh_path}")
|
|
76
|
+
headers = {"Content-Type": "application/json"}
|
|
77
|
+
print("send request")
|
|
78
|
+
response = requests.get(url, data=json.dumps(data), headers=headers, timeout=2000)
|
|
79
|
+
resp_body = response.json()
|
|
80
|
+
if "request_space" in resp_body:
|
|
81
|
+
self.height = resp_body["request_space"]
|
|
82
|
+
def show(self):
|
|
83
|
+
components.iframe("http://127.0.0.1:8080/index.html", width=self.width, height=self.height) # , scrolling=True
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import pyvista as pv
|
|
2
|
+
from pyvista import examples
|
|
3
|
+
from pyvista.trame.ui import plotter_ui
|
|
4
|
+
from trame.app import get_server
|
|
5
|
+
from trame.ui.vuetify3 import VAppLayout
|
|
6
|
+
from trame.widgets import vuetify3 as vuetify
|
|
7
|
+
from trame.widgets import html
|
|
8
|
+
from aiohttp import web
|
|
9
|
+
from trame.decorators import TrameApp, change, controller
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import numpy as np
|
|
14
|
+
import time, threading
|
|
15
|
+
import asyncio
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
|
|
18
|
+
from typing import Dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@TrameApp()
|
|
23
|
+
class MeshViewer:
|
|
24
|
+
|
|
25
|
+
def __init__(self, mesh=None, plotter=None, server=None):
|
|
26
|
+
|
|
27
|
+
pv.OFF_SCREEN = True
|
|
28
|
+
self.scalar_bar_exist = None
|
|
29
|
+
if server is None:
|
|
30
|
+
self.server = get_server()
|
|
31
|
+
else:
|
|
32
|
+
self.server = server
|
|
33
|
+
self.slider_playing = False
|
|
34
|
+
self.path = None
|
|
35
|
+
|
|
36
|
+
self.mesh = None
|
|
37
|
+
self.clean_mesh = None
|
|
38
|
+
|
|
39
|
+
self.style = {
|
|
40
|
+
"background-color": "black",
|
|
41
|
+
"font-color": "white"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
self.pl = self.setup_pl()
|
|
45
|
+
|
|
46
|
+
setattr(self, "on_server_bind", self.server.controller.add("on_server_bind")(self.on_server_bind))
|
|
47
|
+
|
|
48
|
+
# self.replace_mesh(mesh)
|
|
49
|
+
self.my_routes = [
|
|
50
|
+
web.get("/select_mesh", self.change_mesh),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
self.setup_state()
|
|
54
|
+
|
|
55
|
+
self.setup_timer()
|
|
56
|
+
|
|
57
|
+
self.loop = asyncio.get_event_loop()
|
|
58
|
+
|
|
59
|
+
self.mesh_array = None
|
|
60
|
+
|
|
61
|
+
self.state.options = [None]
|
|
62
|
+
self.state.options_warp = ["None", "A", "B"]
|
|
63
|
+
|
|
64
|
+
self.actor = None
|
|
65
|
+
self.width = 800
|
|
66
|
+
self.height = 900
|
|
67
|
+
self.ui = self.build_ui()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def setup_pl(self) -> pv.Plotter:
|
|
72
|
+
pl = pv.Plotter()
|
|
73
|
+
pl.background_color = self.style["background-color"]
|
|
74
|
+
pl.theme.font.color = self.style["font-color"]
|
|
75
|
+
self.bounds_scalar = None
|
|
76
|
+
self.scalar_bar_exist = False
|
|
77
|
+
self.scalar_bar_mapper = None
|
|
78
|
+
return pl
|
|
79
|
+
|
|
80
|
+
def setup_state(self):
|
|
81
|
+
self.state.is_full_screen = False
|
|
82
|
+
self.state.mesh_representation = self.state.options[0] if self.state.options is not None else None
|
|
83
|
+
self.state.warped_field = None
|
|
84
|
+
|
|
85
|
+
self.state.warp_input = 0
|
|
86
|
+
self.state.wireframe_on = True
|
|
87
|
+
self.state.slider_value = 0
|
|
88
|
+
self.state.play_pause_icon = "mdi-play"
|
|
89
|
+
self.prev_bar_repr = self.state.mesh_representation
|
|
90
|
+
self.prev_wrap = "0"
|
|
91
|
+
|
|
92
|
+
def setup_timer(self):
|
|
93
|
+
self.timeout = 0.25
|
|
94
|
+
self.timer = threading.Thread(target=self.timer_callback)
|
|
95
|
+
self.sequence_bounds = [0, 70]
|
|
96
|
+
|
|
97
|
+
@change("mesh_representation")
|
|
98
|
+
def update_mesh_representation(self, mesh_representation, **kwargs):
|
|
99
|
+
|
|
100
|
+
if self.mesh is not None and self.mesh.active_scalars is not None:
|
|
101
|
+
self.pl.remove_scalar_bar()
|
|
102
|
+
|
|
103
|
+
self.prev_bar_repr = mesh_representation
|
|
104
|
+
|
|
105
|
+
if mesh_representation == "None":
|
|
106
|
+
self.state.mesh_representation = None
|
|
107
|
+
for i in range(self.sequence_bounds[1]):
|
|
108
|
+
if self.mesh_array is not None:
|
|
109
|
+
self.mesh_array[i].set_active_scalars(self.state.mesh_representation)
|
|
110
|
+
# self.replace_mesh(self.mesh)
|
|
111
|
+
|
|
112
|
+
self.update_mesh_from_index(self.state.slider_value)
|
|
113
|
+
self.update_warp_input()
|
|
114
|
+
self.ui = self.build_ui()
|
|
115
|
+
# self.update_mesh_from_index(1)
|
|
116
|
+
|
|
117
|
+
@change("warp_input")
|
|
118
|
+
def update_warp_input(self, **kwargs):
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
new_warp = float(self.state.warp_input)
|
|
122
|
+
|
|
123
|
+
dim = self.mesh.point_data.get_array(self.state.mesh_representation).ndim # 1 if scalar, 2 if vector
|
|
124
|
+
if dim == 1:
|
|
125
|
+
new_pyvista_mesh = self.clean_mesh.warp_by_scalar(self.state.warped_field,
|
|
126
|
+
factor=new_warp)
|
|
127
|
+
else:
|
|
128
|
+
new_pyvista_mesh = self.clean_mesh.warp_by_vector(self.state.warped_field,
|
|
129
|
+
factor=new_warp)
|
|
130
|
+
|
|
131
|
+
self.prev_wrap = self.state.warp_input
|
|
132
|
+
self.replace_mesh(new_pyvista_mesh)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
except ValueError as e:
|
|
136
|
+
print(e)
|
|
137
|
+
pass
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(e)
|
|
140
|
+
finally:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
@change("warped_field")
|
|
144
|
+
def update_warped_field(self, warped_field, **kwargs):
|
|
145
|
+
if warped_field is None or warped_field == "None":
|
|
146
|
+
return
|
|
147
|
+
self.update_warp_input(warped_field= warped_field)
|
|
148
|
+
|
|
149
|
+
@change("wireframe_on")
|
|
150
|
+
def update_wireframe_on(self, **kwargs):
|
|
151
|
+
self.replace_mesh(self.mesh)
|
|
152
|
+
|
|
153
|
+
@change("slider_value")
|
|
154
|
+
def slider_value_change(self, slider_value, **kwargs):
|
|
155
|
+
self.update_mesh_from_index(int(slider_value))
|
|
156
|
+
self.state.warp_input = 0
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def state(self):
|
|
160
|
+
return self.server.state
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def ctrl(self):
|
|
164
|
+
return self.server.controller
|
|
165
|
+
|
|
166
|
+
def timer_callback(self):
|
|
167
|
+
|
|
168
|
+
print(self.slider_playing)
|
|
169
|
+
while self.slider_playing and self.server.running:
|
|
170
|
+
self.state.slider_value = (self.state.slider_value + 1) % self.sequence_bounds[1]
|
|
171
|
+
|
|
172
|
+
# self.update_mesh_from_index(self.state.slider_value)
|
|
173
|
+
|
|
174
|
+
self.loop.call_soon_threadsafe(self.update_mesh_from_index, self.state.slider_value)
|
|
175
|
+
self.loop.call_soon_threadsafe(self.server.force_state_push, "slider_value")
|
|
176
|
+
time.sleep(0.25)
|
|
177
|
+
|
|
178
|
+
self.timer = threading.Thread(target=self.timer_callback)
|
|
179
|
+
|
|
180
|
+
def update_mesh_from_index(self, idx):
|
|
181
|
+
if self.mesh_array is not None:
|
|
182
|
+
self.clean_mesh = self.mesh_array[idx]
|
|
183
|
+
self.replace_mesh(self.mesh_array[idx])
|
|
184
|
+
# self.update_warp_input()
|
|
185
|
+
|
|
186
|
+
def handle_new_mesh(self, mesh_path, seq_len):
|
|
187
|
+
self.mesh_array = []
|
|
188
|
+
if seq_len == 1:
|
|
189
|
+
if not os.path.exists(mesh_path):
|
|
190
|
+
print(f"Error, the file {mesh_path} does not exist")
|
|
191
|
+
return
|
|
192
|
+
self.mesh_array.append(pv.read(mesh_path))
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
for i in range(self.sequence_bounds[1]):
|
|
196
|
+
path = self.path % i
|
|
197
|
+
if not os.path.exists(path):
|
|
198
|
+
print(f"Error, the file {path} does not exist")
|
|
199
|
+
return
|
|
200
|
+
self.mesh_array.append(pv.read(path))
|
|
201
|
+
|
|
202
|
+
async def change_mesh(self, request):
|
|
203
|
+
|
|
204
|
+
request_body: Dict[str, str] = await request.json()
|
|
205
|
+
self.path = request_body.get("mesh_path", None)
|
|
206
|
+
self.width = request_body.get("width", self.width)
|
|
207
|
+
self.height = request_body.get("height", self.height)
|
|
208
|
+
self.sequence_bounds[1] = request_body.get("nbr_frames", self.sequence_bounds[1])
|
|
209
|
+
print(f"PATH RECEIVED IS {self.path}")
|
|
210
|
+
if self.path is None:
|
|
211
|
+
print("Error : No filepath found in the change mesh request")
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
self.handle_new_mesh(self.path, self.sequence_bounds[1])
|
|
215
|
+
|
|
216
|
+
self.state.mesh_representation = None
|
|
217
|
+
self.replace_mesh(self.mesh_array[0])
|
|
218
|
+
|
|
219
|
+
self.state.options = self.mesh_array[0].array_names.copy()
|
|
220
|
+
self.state.options.insert(0, "None")
|
|
221
|
+
self.state.options_warp = self.mesh_array[0].array_names.copy()
|
|
222
|
+
self.state.options_warp.insert(0, "None")
|
|
223
|
+
|
|
224
|
+
self.computes_bounds_scalar()
|
|
225
|
+
self.update_ui()
|
|
226
|
+
response_body = {}
|
|
227
|
+
if self.height - self.estimate_controller_height()[1] < 700:
|
|
228
|
+
response_body["request_space"] = 1.65 * self.height
|
|
229
|
+
|
|
230
|
+
self.pl.reset_camera()
|
|
231
|
+
return web.json_response(response_body, status=200)
|
|
232
|
+
|
|
233
|
+
def compute_field_interval(self, field=None):
|
|
234
|
+
if field is None:
|
|
235
|
+
field = self.state.mesh_representation
|
|
236
|
+
if field is None or field == "None":
|
|
237
|
+
field = self.state.options[1]
|
|
238
|
+
max_bound = -np.inf
|
|
239
|
+
min_bound = np.inf
|
|
240
|
+
for i in range(self.sequence_bounds[1]):
|
|
241
|
+
l_max = self.mesh_array[i].get_array(field).max()
|
|
242
|
+
l_min = self.mesh_array[i].get_array(field).min()
|
|
243
|
+
if l_max > max_bound:
|
|
244
|
+
max_bound = l_max
|
|
245
|
+
if l_min < min_bound:
|
|
246
|
+
min_bound = l_min
|
|
247
|
+
return min_bound, max_bound
|
|
248
|
+
|
|
249
|
+
def show_scalar_bar(self, field=None):
|
|
250
|
+
if self.mesh_array is None:
|
|
251
|
+
return
|
|
252
|
+
if field is None:
|
|
253
|
+
field = self.state.mesh_representation
|
|
254
|
+
if field is None:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
if self.bounds_scalar is None:
|
|
258
|
+
self.computes_bounds_scalar()
|
|
259
|
+
|
|
260
|
+
bounds = self.bounds_scalar.get(field, None)
|
|
261
|
+
if bounds is not None:
|
|
262
|
+
self.pl.mapper.lookup_table.SetTableRange(bounds[0], bounds[1])
|
|
263
|
+
self.pl.add_scalar_bar(self.state.mesh_representation)
|
|
264
|
+
|
|
265
|
+
return self.pl.mapper
|
|
266
|
+
|
|
267
|
+
def computes_bounds_scalar(self):
|
|
268
|
+
if self.state.options is None:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
# store bounds and mapper for all the fields available except "None" which is the first one of the options array
|
|
272
|
+
self.bounds_scalar = {}
|
|
273
|
+
|
|
274
|
+
for field in self.state.options[1:]:
|
|
275
|
+
self.bounds_scalar[field] = self.compute_field_interval(field)
|
|
276
|
+
print(self.bounds_scalar)
|
|
277
|
+
|
|
278
|
+
def replace_mesh(self, new_mesh):
|
|
279
|
+
if new_mesh is None:
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
# set custom style
|
|
283
|
+
kwargs_plot = {}
|
|
284
|
+
if self.state.wireframe_on:
|
|
285
|
+
kwargs_plot["style"] = "wireframe"
|
|
286
|
+
|
|
287
|
+
# update mesh and set its active scalar field, as well as adding the scalar bar
|
|
288
|
+
self.mesh = new_mesh
|
|
289
|
+
|
|
290
|
+
# Set the active scalar and create it's scalar bar if it does not exist
|
|
291
|
+
self.mesh.set_active_scalars(self.state.mesh_representation)
|
|
292
|
+
|
|
293
|
+
self.pl.mapper = self.show_scalar_bar(self.state.mesh_representation)
|
|
294
|
+
|
|
295
|
+
# Replace actor with the new mesh (automatically update the actor because they have the same name)
|
|
296
|
+
self.actor = self.pl.add_mesh(self.mesh, style="wireframe" if self.state.wireframe_on else None,
|
|
297
|
+
name="displayed_mesh", show_scalar_bar=True,
|
|
298
|
+
scalar_bar_args={"mapper": self.pl.mapper})
|
|
299
|
+
|
|
300
|
+
def start_server(self):
|
|
301
|
+
try:
|
|
302
|
+
self.server.start()
|
|
303
|
+
except KeyboardInterrupt:
|
|
304
|
+
# Stop the thread that manage the play of the mesh sequence
|
|
305
|
+
self.timer.join() # Wait for the timer thread to finish
|
|
306
|
+
|
|
307
|
+
def update_ui(self):
|
|
308
|
+
if self.mesh is not None and self.mesh.active_scalars is not None:
|
|
309
|
+
self.pl.remove_scalar_bar()
|
|
310
|
+
self.ui = self.build_ui()
|
|
311
|
+
|
|
312
|
+
def option_dropdown(self):
|
|
313
|
+
return vuetify.VSelect(
|
|
314
|
+
v_model=("mesh_representation", "None"),
|
|
315
|
+
items=("options", self.state.options),
|
|
316
|
+
label="Representation",
|
|
317
|
+
hide_details=True,
|
|
318
|
+
dense=True,
|
|
319
|
+
outlined=True,
|
|
320
|
+
classes="pt-1",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def build_slider(self):
|
|
324
|
+
row = html.Div(style='display:flex;justify-content:center;align-content:center;gap:20px;')
|
|
325
|
+
with row:
|
|
326
|
+
with vuetify.VBtn(
|
|
327
|
+
icon=True,
|
|
328
|
+
click=self.play_button
|
|
329
|
+
):
|
|
330
|
+
vuetify.VIcon("{{ play_pause_icon }}")
|
|
331
|
+
html.Div("{{ slider_value }}")
|
|
332
|
+
vuetify.VSlider(
|
|
333
|
+
ref="slider",
|
|
334
|
+
label="",
|
|
335
|
+
min=self.sequence_bounds[0],
|
|
336
|
+
max=self.sequence_bounds[1] - 1,
|
|
337
|
+
v_model=("slider_value", 8),
|
|
338
|
+
step=1
|
|
339
|
+
)
|
|
340
|
+
return row
|
|
341
|
+
|
|
342
|
+
@controller.set("play_button")
|
|
343
|
+
def play_button(self):
|
|
344
|
+
if self.sequence_bounds[1] <= 1:
|
|
345
|
+
print("Impossible to start the sequence since it's a unique mesh")
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
# Invert the state of the play button and if it's playing start the timer updating frame at a fixed interval
|
|
349
|
+
self.slider_playing = not self.slider_playing
|
|
350
|
+
if self.slider_playing and not self.timer.is_alive():
|
|
351
|
+
self.state.play_pause_icon = "mdi-pause"
|
|
352
|
+
|
|
353
|
+
self.timer.start()
|
|
354
|
+
else:
|
|
355
|
+
self.state.play_pause_icon = "mdi-play"
|
|
356
|
+
self.update_ui()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def build_warp_option(self):
|
|
360
|
+
return vuetify.VSelect(
|
|
361
|
+
v_model=("warped_field"),
|
|
362
|
+
items=("options_warp", self.state.options_warp),
|
|
363
|
+
label="Warped Field",
|
|
364
|
+
hide_details=True,
|
|
365
|
+
dense=True,
|
|
366
|
+
outlined=True,
|
|
367
|
+
classes="pt-1",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def build_warper(self):
|
|
371
|
+
warper = vuetify.VCol(cols="6")
|
|
372
|
+
with warper:
|
|
373
|
+
print("array of options ------------------")
|
|
374
|
+
print(self.state.options_warp)
|
|
375
|
+
options = ['None', 'U', 'V', 'A', 'E', 'dE']
|
|
376
|
+
options = self.state.options_warp
|
|
377
|
+
print(options)
|
|
378
|
+
self.build_warp_option()
|
|
379
|
+
|
|
380
|
+
# self.option_dropdown(field_linked="warped_field", options=self.state.options)
|
|
381
|
+
if self.state.mesh_representation != "None" and self.state.mesh_representation is not None:
|
|
382
|
+
|
|
383
|
+
html.Input(type="number", label="warp",
|
|
384
|
+
v_model=("warp_input", 0.0),
|
|
385
|
+
ref=f"warp-input",
|
|
386
|
+
step="1"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return warper
|
|
390
|
+
|
|
391
|
+
def build_mesh_control_layout(self):
|
|
392
|
+
layout = html.Div()
|
|
393
|
+
with (layout):
|
|
394
|
+
with vuetify.VRow(dense=True):
|
|
395
|
+
with vuetify.VCol(cols="6"):
|
|
396
|
+
if self.state.options[0] is not None:
|
|
397
|
+
self.option_dropdown()
|
|
398
|
+
self.build_warper()
|
|
399
|
+
vuetify.VCheckbox(v_model=("wireframe_on",), label="Wireframe on")
|
|
400
|
+
if self.sequence_bounds[1] != 1:
|
|
401
|
+
self.build_slider()
|
|
402
|
+
|
|
403
|
+
with vuetify.VBtn(click=self.request_full, style="position: absolute; bottom:25px; right:25px;", icon=True):
|
|
404
|
+
vuetify.VIcon("mdi-fullscreen" if not self.state.is_full_screen else "mdi-fullscreen-exit")
|
|
405
|
+
return layout
|
|
406
|
+
|
|
407
|
+
def estimate_controller_height(self):
|
|
408
|
+
if self.mesh_array is not None and self.state.mesh_representation is not None:
|
|
409
|
+
ndim = self.mesh_array[0].get_array(self.state.mesh_representation).shape[1]
|
|
410
|
+
return 30 * ndim, 9 * 30
|
|
411
|
+
return 0.2 * self.height, 9 * 30
|
|
412
|
+
|
|
413
|
+
def build_ui(self):
|
|
414
|
+
control_height = self.estimate_controller_height()[0]
|
|
415
|
+
if not self.state.is_full_screen:
|
|
416
|
+
plotter_height = self.height - control_height
|
|
417
|
+
plotter_height = f"{plotter_height}px"
|
|
418
|
+
width = f"{self.width}px"
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
else:
|
|
422
|
+
plotter_height = "100%"
|
|
423
|
+
width = "100%"
|
|
424
|
+
|
|
425
|
+
print(self.height)
|
|
426
|
+
|
|
427
|
+
with VAppLayout(self.server) as layout:
|
|
428
|
+
with layout.root:
|
|
429
|
+
with html.Div(
|
|
430
|
+
style=f"height: {self.height}px ;width:{width}"): # add the following arg: style="height: 100vh; width:100vw" to have the plotter taking all screen
|
|
431
|
+
with html.Div(ref="container", style=f"height:{plotter_height};padding: 0;"):
|
|
432
|
+
with vuetify.VCol(style=f"height:{plotter_height};padding: 0;"):
|
|
433
|
+
if self.slider_playing:
|
|
434
|
+
plotter_ui(self.pl, default_server_rendering=True, mode="server",
|
|
435
|
+
style="width: 100%; height:100%;background-color: black;")
|
|
436
|
+
else:
|
|
437
|
+
plotter_ui(self.pl, default_server_rendering=True,
|
|
438
|
+
style="width: 100%; height:100%;background-color: black;")
|
|
439
|
+
|
|
440
|
+
with vuetify.VCol(style=f"height:{control_height}px;padding: 0;width:100%;"):
|
|
441
|
+
self.build_mesh_control_layout()
|
|
442
|
+
return layout
|
|
443
|
+
|
|
444
|
+
def on_server_bind(self, wslink_server):
|
|
445
|
+
wslink_server.app.add_routes(self.my_routes)
|
|
446
|
+
|
|
447
|
+
def request_full(self):
|
|
448
|
+
self.server.js_call("container", "requestFullscreen")
|
|
449
|
+
# if not self.state.is_full_screen:
|
|
450
|
+
# self.server.js_call("container", "requestFullscreen")
|
|
451
|
+
# else:
|
|
452
|
+
# self.server.js_call("", "exitFullscreen()")
|
|
453
|
+
self.state.is_full_screen = not self.state.is_full_screen
|
|
454
|
+
self.update_ui()
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class Component(ABC):
|
|
458
|
+
def __init__(self, state, ctrl):
|
|
459
|
+
self.supported_handler = ["change"]
|
|
460
|
+
self.state = state
|
|
461
|
+
self.ctrl = ctrl
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# add corresponding decorator to fucntion (change or set)
|
|
465
|
+
@abstractmethod
|
|
466
|
+
def handler(self, new_value, **kwargs):
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
@abstractmethod
|
|
470
|
+
def render(self):
|
|
471
|
+
pass
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
if __name__ == "__main__":
|
|
476
|
+
mv = MeshViewer()
|
|
477
|
+
mv.start_server()
|
|
478
|
+
# sm = ServerManager()
|
|
479
|
+
# sm.start_server()
|