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.
@@ -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,4 @@
1
+ # streamlit-pyvista
2
+ A Streamlit component that allow support for new pyvista viewer backend : Trame.
3
+
4
+
@@ -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,2 @@
1
+ from .trame_viewer import MeshViewer
2
+ from .MeshViewerComponent import MeshViewerComponent
@@ -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()