ngsolve-gui 0.0.1__py3-none-any.whl
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.
- ngsolve_gui/__init__.py +3 -0
- ngsolve_gui/__main__.py +4 -0
- ngsolve_gui/app.py +514 -0
- ngsolve_gui/app_data.py +134 -0
- ngsolve_gui/appconfig.py +14 -0
- ngsolve_gui/assets/logo_withname_retina.png +3 -0
- ngsolve_gui/file_loader.py +278 -0
- ngsolve_gui/function.py +676 -0
- ngsolve_gui/geometry.py +498 -0
- ngsolve_gui/mesh.py +260 -0
- ngsolve_gui/navigator.py +211 -0
- ngsolve_gui/pick_overlay.py +29 -0
- ngsolve_gui/plot.py +118 -0
- ngsolve_gui/property_panel.py +77 -0
- ngsolve_gui/region_colors.py +461 -0
- ngsolve_gui/registry.py +29 -0
- ngsolve_gui/run.py +45 -0
- ngsolve_gui/sections/__init__.py +11 -0
- ngsolve_gui/sections/clipping.py +217 -0
- ngsolve_gui/sections/colorbar.py +95 -0
- ngsolve_gui/sections/deformation.py +33 -0
- ngsolve_gui/sections/entity_numbers.py +13 -0
- ngsolve_gui/sections/fieldlines.py +78 -0
- ngsolve_gui/sections/function_options.py +98 -0
- ngsolve_gui/sections/geometry_options.py +95 -0
- ngsolve_gui/sections/geometry_selection.py +111 -0
- ngsolve_gui/sections/mesh_colors.py +101 -0
- ngsolve_gui/sections/mesh_view.py +86 -0
- ngsolve_gui/sections/vectors.py +112 -0
- ngsolve_gui/styles.py +84 -0
- ngsolve_gui/system_monitor.py +179 -0
- ngsolve_gui/webgpu_tab.py +408 -0
- ngsolve_gui-0.0.1.dist-info/METADATA +7 -0
- ngsolve_gui-0.0.1.dist-info/RECORD +37 -0
- ngsolve_gui-0.0.1.dist-info/WHEEL +5 -0
- ngsolve_gui-0.0.1.dist-info/entry_points.txt +2 -0
- ngsolve_gui-0.0.1.dist-info/top_level.txt +1 -0
ngsolve_gui/__init__.py
ADDED
ngsolve_gui/__main__.py
ADDED
ngsolve_gui/app.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from ngapp.app import App
|
|
7
|
+
from ngapp.components import *
|
|
8
|
+
|
|
9
|
+
from .app_data import AppData
|
|
10
|
+
from .file_loader import load_file
|
|
11
|
+
from ngapp.keybindings import KeybindingManager, keybinding_styles
|
|
12
|
+
from .navigator import Navigator
|
|
13
|
+
from .property_panel import PropertyPanel
|
|
14
|
+
from .styles import css, theme, flex_fill, panel_full
|
|
15
|
+
from .system_monitor import SystemMonitor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Panel(Div):
|
|
19
|
+
def __init__(self, app_data):
|
|
20
|
+
self.app_data = app_data
|
|
21
|
+
self.comp = None
|
|
22
|
+
super().__init__(ui_class=str(panel_full))
|
|
23
|
+
self.set_tab()
|
|
24
|
+
|
|
25
|
+
def set_tab(self):
|
|
26
|
+
name = self.app_data.active_tab
|
|
27
|
+
if name is None:
|
|
28
|
+
self.ui_children = []
|
|
29
|
+
return
|
|
30
|
+
tab = self.app_data.get_tab(name)
|
|
31
|
+
if tab is None:
|
|
32
|
+
self.ui_children = []
|
|
33
|
+
return
|
|
34
|
+
if "component" in tab:
|
|
35
|
+
self.comp = comp = tab["component"]
|
|
36
|
+
else:
|
|
37
|
+
cls = self._resolve_class(tab["type"])
|
|
38
|
+
self.comp = comp = cls(
|
|
39
|
+
tab["name"],
|
|
40
|
+
tab["data"],
|
|
41
|
+
app_data=self.app_data,
|
|
42
|
+
)
|
|
43
|
+
tab["component"] = comp
|
|
44
|
+
self.ui_children = [comp]
|
|
45
|
+
|
|
46
|
+
def _resolve_class(self, type_key):
|
|
47
|
+
from .registry import get_component_info
|
|
48
|
+
|
|
49
|
+
info = get_component_info(type_key)
|
|
50
|
+
if info is None:
|
|
51
|
+
raise ValueError(f"Unknown component type: {type_key}")
|
|
52
|
+
return info["cls"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Settings(QMenu):
|
|
56
|
+
def __init__(self, app):
|
|
57
|
+
self.app = app
|
|
58
|
+
val = self.app.usersettings.get("nthreads", 0)
|
|
59
|
+
nthreads = QInput(
|
|
60
|
+
QTooltip(
|
|
61
|
+
"Set number of threads used by NGSolve, 0 for all available cores. Only takes effect after restarting the application."
|
|
62
|
+
),
|
|
63
|
+
ui_label="Number of Threads",
|
|
64
|
+
ui_type="number",
|
|
65
|
+
ui_model_value=val,
|
|
66
|
+
)
|
|
67
|
+
nthreads.on_update_model_value(self.app.usersettings.update("nthreads"))
|
|
68
|
+
|
|
69
|
+
show_axes = QCheckbox(
|
|
70
|
+
ui_label="Show Axes by Default",
|
|
71
|
+
ui_model_value=self.app.usersettings.get("axes_visible", True),
|
|
72
|
+
)
|
|
73
|
+
show_axes.on_update_model_value(self.app.usersettings.update("axes_visible"))
|
|
74
|
+
|
|
75
|
+
show_navcube = QCheckbox(
|
|
76
|
+
ui_label="Show Navigation Cube by Default",
|
|
77
|
+
ui_model_value=self.app.usersettings.get("navcube_visible", False),
|
|
78
|
+
)
|
|
79
|
+
show_navcube.on_update_model_value(self.app.usersettings.update("navcube_visible"))
|
|
80
|
+
|
|
81
|
+
scale_by_mag = QCheckbox(
|
|
82
|
+
ui_label="Scale Vectors by Magnitude by Default",
|
|
83
|
+
ui_model_value=self.app.usersettings.get("scale_by_magnitude", True),
|
|
84
|
+
)
|
|
85
|
+
scale_by_mag.on_update_model_value(self.app.usersettings.update("scale_by_magnitude"))
|
|
86
|
+
|
|
87
|
+
super().__init__(QCard(
|
|
88
|
+
QCardSection("Settings"),
|
|
89
|
+
QCardSection(nthreads, show_axes, show_navcube, scale_by_mag),
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class StatusBar(Div):
|
|
94
|
+
"""Floating pill overlay at the bottom of the scene showing loading progress."""
|
|
95
|
+
|
|
96
|
+
# Outer wrapper — anchored to the bottom-center of the nearest
|
|
97
|
+
# position:relative ancestor (the scene container).
|
|
98
|
+
_VISIBLE = (
|
|
99
|
+
"position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); "
|
|
100
|
+
"z-index: 1000; display: flex; flex-direction: column; align-items: stretch; "
|
|
101
|
+
"background: rgba(15,23,42,0.88); backdrop-filter: blur(6px); "
|
|
102
|
+
"border-radius: 12px; padding: 10px 18px 12px; min-width: 320px; "
|
|
103
|
+
"max-width: 480px; box-shadow: 0 4px 24px rgba(0,0,0,0.25); "
|
|
104
|
+
"color: #e2e8f0; font-size: 0.82rem;"
|
|
105
|
+
)
|
|
106
|
+
_HIDDEN = "display: none;"
|
|
107
|
+
|
|
108
|
+
def __init__(self):
|
|
109
|
+
# Top row: icon + label + cancel
|
|
110
|
+
self._label = Div(
|
|
111
|
+
"",
|
|
112
|
+
ui_style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",
|
|
113
|
+
)
|
|
114
|
+
self._pct_label = Div(
|
|
115
|
+
"",
|
|
116
|
+
ui_style=(
|
|
117
|
+
"white-space: nowrap; font-variant-numeric: tabular-nums; "
|
|
118
|
+
"font-size: 0.78rem; color: #94a3b8; min-width: 36px; text-align: right;"
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
self._cancel_btn = QBtn(
|
|
122
|
+
QTooltip("Cancel"),
|
|
123
|
+
ui_icon="mdi-close",
|
|
124
|
+
ui_flat=True,
|
|
125
|
+
ui_dense=True,
|
|
126
|
+
ui_round=True,
|
|
127
|
+
ui_size="xs",
|
|
128
|
+
ui_padding="2px",
|
|
129
|
+
ui_color="grey-5",
|
|
130
|
+
)
|
|
131
|
+
self._cancel_btn.on_click(self._on_cancel)
|
|
132
|
+
|
|
133
|
+
top_row = Div(
|
|
134
|
+
QSpinner(ui_color="accent", ui_size="18px"),
|
|
135
|
+
self._label,
|
|
136
|
+
QSpace(),
|
|
137
|
+
self._pct_label,
|
|
138
|
+
self._cancel_btn,
|
|
139
|
+
ui_style="display: flex; align-items: center; gap: 8px;",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Progress bar (track + filled portion)
|
|
143
|
+
self._bar_fill = Div(
|
|
144
|
+
ui_style=(
|
|
145
|
+
"height: 100%; width: 0%; border-radius: 3px; "
|
|
146
|
+
"background: linear-gradient(90deg, #14B8A6, #0EA5E9); "
|
|
147
|
+
"transition: width 0.3s ease;"
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
bar_track = Div(
|
|
151
|
+
self._bar_fill,
|
|
152
|
+
ui_style=(
|
|
153
|
+
"height: 6px; border-radius: 3px; "
|
|
154
|
+
"background: rgba(255,255,255,0.12); margin-top: 8px; "
|
|
155
|
+
"overflow: hidden;"
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self._thread = None
|
|
160
|
+
self._done_event = None
|
|
161
|
+
self._generation = 0
|
|
162
|
+
|
|
163
|
+
super().__init__(top_row, bar_track, ui_style=self._HIDDEN)
|
|
164
|
+
|
|
165
|
+
def show(self, filename, thread, done_event):
|
|
166
|
+
self._generation += 1
|
|
167
|
+
self._thread = thread
|
|
168
|
+
self._done_event = done_event
|
|
169
|
+
self._thread_name = thread.name if thread else ""
|
|
170
|
+
self._label.ui_children = [f"Running {filename} \u2026"]
|
|
171
|
+
self._pct_label.ui_children = [""]
|
|
172
|
+
self._bar_fill.ui_style = (
|
|
173
|
+
"height: 100%; width: 100%; border-radius: 3px; "
|
|
174
|
+
"background: linear-gradient(90deg, #14B8A6, #0EA5E9); "
|
|
175
|
+
"animation: indeterminate 1.4s ease infinite; "
|
|
176
|
+
"transition: none;"
|
|
177
|
+
)
|
|
178
|
+
self.ui_style = self._VISIBLE
|
|
179
|
+
self._start_poll(self._generation)
|
|
180
|
+
|
|
181
|
+
def hide(self):
|
|
182
|
+
self._thread = None
|
|
183
|
+
self._done_event = None
|
|
184
|
+
self.ui_style = self._HIDDEN
|
|
185
|
+
|
|
186
|
+
def _set_progress(self, percent):
|
|
187
|
+
"""Set determinate progress (0–100)."""
|
|
188
|
+
w = max(0, min(100, percent))
|
|
189
|
+
self._bar_fill.ui_style = (
|
|
190
|
+
f"height: 100%; width: {w:.1f}%; border-radius: 3px; "
|
|
191
|
+
"background: linear-gradient(90deg, #14B8A6, #0EA5E9); "
|
|
192
|
+
"transition: width 0.3s ease;"
|
|
193
|
+
)
|
|
194
|
+
self._pct_label.ui_children = [f"{w:.0f}%"]
|
|
195
|
+
|
|
196
|
+
def _set_indeterminate(self):
|
|
197
|
+
self._bar_fill.ui_style = (
|
|
198
|
+
"height: 100%; width: 100%; border-radius: 3px; "
|
|
199
|
+
"background: linear-gradient(90deg, #14B8A6, #0EA5E9); "
|
|
200
|
+
"animation: indeterminate 1.4s ease infinite; "
|
|
201
|
+
"transition: none;"
|
|
202
|
+
)
|
|
203
|
+
self._pct_label.ui_children = [""]
|
|
204
|
+
|
|
205
|
+
def _start_poll(self, gen):
|
|
206
|
+
def poll():
|
|
207
|
+
from netgen.libngpy._meshing import _GetStatus
|
|
208
|
+
|
|
209
|
+
while gen == self._generation:
|
|
210
|
+
time.sleep(0.3)
|
|
211
|
+
done_event = self._done_event
|
|
212
|
+
if done_event is None:
|
|
213
|
+
break
|
|
214
|
+
try:
|
|
215
|
+
status_text, percent = _GetStatus()
|
|
216
|
+
except Exception:
|
|
217
|
+
status_text, percent = "idle", 0.0
|
|
218
|
+
|
|
219
|
+
done = done_event.is_set()
|
|
220
|
+
|
|
221
|
+
if status_text and status_text != "idle":
|
|
222
|
+
self._label.ui_children = [status_text]
|
|
223
|
+
if percent > 0:
|
|
224
|
+
self._set_progress(percent)
|
|
225
|
+
else:
|
|
226
|
+
self._set_indeterminate()
|
|
227
|
+
# Script finished but netgen hasn't reset status yet
|
|
228
|
+
if done:
|
|
229
|
+
self.hide()
|
|
230
|
+
break
|
|
231
|
+
elif done:
|
|
232
|
+
self.hide()
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
threading.Thread(target=poll, daemon=True, name="StatusPoll").start()
|
|
236
|
+
|
|
237
|
+
def _on_cancel(self):
|
|
238
|
+
self._generation += 1
|
|
239
|
+
thread = self._thread
|
|
240
|
+
# Only interrupt non-IPython threads; the IPython shell stays
|
|
241
|
+
# alive for interactive use — just dismiss the pill.
|
|
242
|
+
if (
|
|
243
|
+
thread
|
|
244
|
+
and thread.is_alive()
|
|
245
|
+
and self._thread_name != "IPythonEmbedder"
|
|
246
|
+
):
|
|
247
|
+
try:
|
|
248
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
249
|
+
ctypes.c_ulong(thread.ident),
|
|
250
|
+
ctypes.py_object(KeyboardInterrupt),
|
|
251
|
+
)
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
self.hide()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class NGSolveGui(App):
|
|
258
|
+
def __init__(self, filename=None, local_path=None):
|
|
259
|
+
self._local_path = local_path if local_path else os.path.expanduser("~")
|
|
260
|
+
self.app_data = AppData()
|
|
261
|
+
|
|
262
|
+
# Toolbar buttons
|
|
263
|
+
upload_file = QBtn(QTooltip("Load File"), ui_flat=True, ui_icon="mdi-plus")
|
|
264
|
+
upload_file.on_click(self._load_file)
|
|
265
|
+
savebtn = QBtn(
|
|
266
|
+
QTooltip("Save Project"), ui_flat=True, ui_icon="mdi-content-save"
|
|
267
|
+
)
|
|
268
|
+
savebtn.on_click(self.save_local)
|
|
269
|
+
loadbtn = QBtn(
|
|
270
|
+
QTooltip("Load Project"), ui_flat=True, ui_icon="mdi-folder-open"
|
|
271
|
+
)
|
|
272
|
+
loadbtn.on_click(self.load_local)
|
|
273
|
+
|
|
274
|
+
# Panel toggle buttons
|
|
275
|
+
self._nav_btn = QBtn(
|
|
276
|
+
QTooltip("Toggle Navigator"),
|
|
277
|
+
ui_flat=True,
|
|
278
|
+
ui_icon="mdi-page-layout-sidebar-left",
|
|
279
|
+
)
|
|
280
|
+
self._nav_btn.on_click(self._toggle_navigator)
|
|
281
|
+
self._prop_btn = QBtn(
|
|
282
|
+
QTooltip("Toggle Properties"),
|
|
283
|
+
ui_flat=True,
|
|
284
|
+
ui_icon="mdi-page-layout-sidebar-right",
|
|
285
|
+
)
|
|
286
|
+
self._prop_btn.on_click(self._toggle_property_panel)
|
|
287
|
+
|
|
288
|
+
settings_btn = QBtn(
|
|
289
|
+
Settings(self), QTooltip("User Settings"), ui_flat=True, ui_icon="mdi-cog"
|
|
290
|
+
)
|
|
291
|
+
close_btn = QBtn(QTooltip("Quit"), ui_flat=True, ui_icon="mdi-close")
|
|
292
|
+
close_btn.on_click(self.quit)
|
|
293
|
+
ngs_logo = Div(
|
|
294
|
+
QImg(
|
|
295
|
+
ui_src=self.load_asset("logo_withname_retina.png"),
|
|
296
|
+
ui_height="40px",
|
|
297
|
+
ui_fit="scale-down",
|
|
298
|
+
),
|
|
299
|
+
ui_style="width: 200px;",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self.system_monitor = SystemMonitor()
|
|
303
|
+
|
|
304
|
+
bar = QBar(
|
|
305
|
+
ngs_logo,
|
|
306
|
+
upload_file,
|
|
307
|
+
savebtn,
|
|
308
|
+
loadbtn,
|
|
309
|
+
QSpace(),
|
|
310
|
+
self.system_monitor,
|
|
311
|
+
self._nav_btn,
|
|
312
|
+
self._prop_btn,
|
|
313
|
+
settings_btn,
|
|
314
|
+
close_btn,
|
|
315
|
+
ui_style="height: 60px",
|
|
316
|
+
ui_class="bg-primary text-grey-4",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Three-column layout using flex
|
|
320
|
+
self.navigator = Navigator(self.app_data, self._click_tab)
|
|
321
|
+
self.property_panel = PropertyPanel()
|
|
322
|
+
self.tab_panel = Panel(self.app_data)
|
|
323
|
+
self.status_bar = StatusBar()
|
|
324
|
+
|
|
325
|
+
self._nav_visible = self.usersettings.get("nav_visible", True)
|
|
326
|
+
self._prop_visible = self.usersettings.get("prop_visible", True)
|
|
327
|
+
self._nav_width = self.usersettings.get("nav_width", 200)
|
|
328
|
+
self._prop_width = self.usersettings.get("prop_width", 280)
|
|
329
|
+
|
|
330
|
+
self.kb = KeybindingManager(self, theme=theme)
|
|
331
|
+
|
|
332
|
+
# Inner splitter: center | property panel (reverse so model = prop width)
|
|
333
|
+
self._inner_splitter = QSplitter(
|
|
334
|
+
ui_model_value=self._prop_width if self._prop_visible else 0,
|
|
335
|
+
ui_unit="px",
|
|
336
|
+
ui_reverse=True,
|
|
337
|
+
ui_limits=[0, 500] if self._prop_visible else [0, 0],
|
|
338
|
+
ui_emit_immediately=True,
|
|
339
|
+
ui_slots={
|
|
340
|
+
"before": [Div(self.tab_panel, ui_class=str(flex_fill))],
|
|
341
|
+
"after": [self.property_panel],
|
|
342
|
+
},
|
|
343
|
+
ui_style="height: calc(100vh - 60px);",
|
|
344
|
+
)
|
|
345
|
+
self._inner_splitter.on_update_model_value(self._on_prop_width_change)
|
|
346
|
+
|
|
347
|
+
# Outer splitter: navigator | inner splitter
|
|
348
|
+
self._outer_splitter = QSplitter(
|
|
349
|
+
ui_model_value=self._nav_width if self._nav_visible else 0,
|
|
350
|
+
ui_unit="px",
|
|
351
|
+
ui_limits=[0, 500] if self._nav_visible else [0, 0],
|
|
352
|
+
ui_emit_immediately=True,
|
|
353
|
+
ui_slots={
|
|
354
|
+
"before": [self.navigator],
|
|
355
|
+
"after": [self._inner_splitter],
|
|
356
|
+
},
|
|
357
|
+
ui_style="height: calc(100vh - 60px);",
|
|
358
|
+
)
|
|
359
|
+
self._outer_splitter.on_update_model_value(self._on_nav_width_change)
|
|
360
|
+
|
|
361
|
+
page = self._outer_splitter
|
|
362
|
+
|
|
363
|
+
super().__init__(bar, page, self.status_bar, self.kb.indicator, self.kb.help_overlay)
|
|
364
|
+
|
|
365
|
+
theme.apply(self)
|
|
366
|
+
css.inject(self)
|
|
367
|
+
keybinding_styles.inject(self)
|
|
368
|
+
self._inject_status_bar_css()
|
|
369
|
+
self.on_load(self.__on_load)
|
|
370
|
+
|
|
371
|
+
# -- Global keybindings (always active) --
|
|
372
|
+
kb = self.kb
|
|
373
|
+
kb.add("h", kb.toggle_help, "Show keyboard shortcuts", "General")
|
|
374
|
+
kb.add("ctrl+b", self._toggle_navigator, "Toggle navigator", "Panels")
|
|
375
|
+
kb.add(
|
|
376
|
+
"ctrl+alt+b", self._toggle_property_panel, "Toggle property panel", "Panels"
|
|
377
|
+
)
|
|
378
|
+
for i in range(1, 10):
|
|
379
|
+
kb.add(
|
|
380
|
+
str(i),
|
|
381
|
+
lambda n=i: self.navigator.select_by_index(n),
|
|
382
|
+
f"Select item {i}",
|
|
383
|
+
"Navigation",
|
|
384
|
+
)
|
|
385
|
+
self.add_keybinding("escape", lambda e: self.kb.on_escape())
|
|
386
|
+
self.on_before_save(self.__on_before_save)
|
|
387
|
+
if isinstance(filename, str):
|
|
388
|
+
self._load_with_status(filename)
|
|
389
|
+
elif isinstance(filename, list):
|
|
390
|
+
for f in filename:
|
|
391
|
+
self._load_with_status(f)
|
|
392
|
+
|
|
393
|
+
def _inject_status_bar_css(self):
|
|
394
|
+
kf = (
|
|
395
|
+
"@keyframes indeterminate {"
|
|
396
|
+
" 0% { transform: translateX(-100%); }"
|
|
397
|
+
" 100% { transform: translateX(100%); }"
|
|
398
|
+
"}"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def _inject(js):
|
|
402
|
+
el = js.document.createElement("style")
|
|
403
|
+
el.textContent = kf
|
|
404
|
+
js.document.head.appendChild(el)
|
|
405
|
+
|
|
406
|
+
self.call_js(_inject)
|
|
407
|
+
|
|
408
|
+
def _load_file(self):
|
|
409
|
+
from tkinter import Tk, filedialog
|
|
410
|
+
|
|
411
|
+
root = Tk()
|
|
412
|
+
root.withdraw()
|
|
413
|
+
file_path = filedialog.askopenfilename(
|
|
414
|
+
title="Select a file",
|
|
415
|
+
initialdir=self._local_path,
|
|
416
|
+
filetypes=[
|
|
417
|
+
("All Files", "*.*"),
|
|
418
|
+
("Mesh Files", "*.vol *.vol.gz"),
|
|
419
|
+
("Geometry Files", "*.step *.iges *.stp"),
|
|
420
|
+
],
|
|
421
|
+
)
|
|
422
|
+
root.destroy()
|
|
423
|
+
if file_path:
|
|
424
|
+
self._local_path = os.path.dirname(file_path)
|
|
425
|
+
self._load_with_status(file_path)
|
|
426
|
+
|
|
427
|
+
def _load_with_status(self, filename):
|
|
428
|
+
if not filename:
|
|
429
|
+
return
|
|
430
|
+
result = load_file(filename, self)
|
|
431
|
+
if result:
|
|
432
|
+
thread, done_event = result
|
|
433
|
+
name = os.path.basename(str(filename))
|
|
434
|
+
self.status_bar.show(name, thread, done_event)
|
|
435
|
+
|
|
436
|
+
def __on_before_save(self):
|
|
437
|
+
self.storage.set("app_data", self.app_data.get_save_data(), use_pickle=True)
|
|
438
|
+
|
|
439
|
+
def __on_load(self):
|
|
440
|
+
data = self.storage.get("app_data")
|
|
441
|
+
if data is not None:
|
|
442
|
+
self.app_data._data.update(data)
|
|
443
|
+
self._update()
|
|
444
|
+
self.app_data._update = self._update
|
|
445
|
+
|
|
446
|
+
def _click_tab(self, tabname):
|
|
447
|
+
self.app_data.active_tab = tabname
|
|
448
|
+
self.tab_panel.set_tab()
|
|
449
|
+
self.navigator.update()
|
|
450
|
+
comp = self.tab_panel.comp
|
|
451
|
+
tab = self.app_data.get_tab(tabname)
|
|
452
|
+
type_key = tab.get("type", "") if tab else ""
|
|
453
|
+
self.property_panel.set_component(comp, type_key)
|
|
454
|
+
self.kb.set_component(comp)
|
|
455
|
+
|
|
456
|
+
def _toggle_navigator(self):
|
|
457
|
+
self._nav_visible = not self._nav_visible
|
|
458
|
+
self.usersettings.set("nav_visible", self._nav_visible)
|
|
459
|
+
self._apply_panel_visibility()
|
|
460
|
+
|
|
461
|
+
def _toggle_property_panel(self):
|
|
462
|
+
self._prop_visible = not self._prop_visible
|
|
463
|
+
self.usersettings.set("prop_visible", self._prop_visible)
|
|
464
|
+
self._apply_panel_visibility()
|
|
465
|
+
|
|
466
|
+
def _on_nav_width_change(self, event):
|
|
467
|
+
val = int(event.value)
|
|
468
|
+
if val > 0:
|
|
469
|
+
self._nav_width = val
|
|
470
|
+
self.usersettings.set("nav_width", val)
|
|
471
|
+
|
|
472
|
+
def _on_prop_width_change(self, event):
|
|
473
|
+
val = int(event.value)
|
|
474
|
+
if val > 0:
|
|
475
|
+
self._prop_width = val
|
|
476
|
+
self.usersettings.set("prop_width", val)
|
|
477
|
+
|
|
478
|
+
def _apply_panel_visibility(self):
|
|
479
|
+
if not hasattr(self, "_outer_splitter"):
|
|
480
|
+
return
|
|
481
|
+
if self._nav_visible:
|
|
482
|
+
self._outer_splitter.ui_model_value = self._nav_width
|
|
483
|
+
self._outer_splitter.ui_limits = [0, 500]
|
|
484
|
+
else:
|
|
485
|
+
self._outer_splitter.ui_model_value = 0
|
|
486
|
+
self._outer_splitter.ui_limits = [0, 0]
|
|
487
|
+
if self._prop_visible:
|
|
488
|
+
self._inner_splitter.ui_model_value = self._prop_width
|
|
489
|
+
self._inner_splitter.ui_limits = [0, 500]
|
|
490
|
+
else:
|
|
491
|
+
self._inner_splitter.ui_model_value = 0
|
|
492
|
+
self._inner_splitter.ui_limits = [0, 0]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def redraw(self, *args, **kwargs):
|
|
496
|
+
self.app_data.set_needs_redraw()
|
|
497
|
+
comp = self.tab_panel.comp
|
|
498
|
+
if comp is not None:
|
|
499
|
+
if hasattr(comp, "redraw"):
|
|
500
|
+
comp.redraw()
|
|
501
|
+
|
|
502
|
+
def _update(self):
|
|
503
|
+
self.navigator.update()
|
|
504
|
+
self.tab_panel.set_tab()
|
|
505
|
+
active = self.app_data.active_tab
|
|
506
|
+
if active:
|
|
507
|
+
comp = self.tab_panel.comp
|
|
508
|
+
tab = self.app_data.get_tab(active)
|
|
509
|
+
type_key = tab.get("type", "") if tab else ""
|
|
510
|
+
self.property_panel.set_component(comp, type_key)
|
|
511
|
+
self.kb.set_component(comp)
|
|
512
|
+
else:
|
|
513
|
+
self.property_panel.set_component(None, "")
|
|
514
|
+
self.kb.set_component(None)
|
ngsolve_gui/app_data.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from ngsolve_webgpu import *
|
|
2
|
+
from webgpu.camera import Camera
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AppData:
|
|
6
|
+
_data: dict
|
|
7
|
+
_gpu_cache: dict
|
|
8
|
+
_clipping: Clipping
|
|
9
|
+
_camera: Camera
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self._data = {"tabs": {}, "active_tab": None}
|
|
13
|
+
self._update = None
|
|
14
|
+
self._gpu_cache = {}
|
|
15
|
+
self._clipping = Clipping()
|
|
16
|
+
self._camera = Camera()
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def clipping(self):
|
|
20
|
+
return self._clipping
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def camera(self):
|
|
24
|
+
return self._camera
|
|
25
|
+
|
|
26
|
+
def get_mesh_gpu_data(self, mesh):
|
|
27
|
+
key = repr(mesh)
|
|
28
|
+
if key not in self._gpu_cache:
|
|
29
|
+
self._gpu_cache[key] = MeshData(mesh)
|
|
30
|
+
return self._gpu_cache[key]
|
|
31
|
+
|
|
32
|
+
def get_function_gpu_data(self, cf, mesh, **kwargs):
|
|
33
|
+
key = hash((repr(cf), repr(mesh), tuple(sorted(kwargs.items()))))
|
|
34
|
+
if key not in self._gpu_cache:
|
|
35
|
+
mdata = self.get_mesh_gpu_data(mesh)
|
|
36
|
+
self._gpu_cache[key] = FunctionData(mdata, cf, **kwargs)
|
|
37
|
+
return self._gpu_cache[key]
|
|
38
|
+
|
|
39
|
+
def set_needs_redraw(self):
|
|
40
|
+
for tab in self._data["tabs"].values():
|
|
41
|
+
if "component" in tab:
|
|
42
|
+
tab["component"]._redraw_needed = True
|
|
43
|
+
|
|
44
|
+
def get_save_data(self):
|
|
45
|
+
from ngapp.observable import snapshot
|
|
46
|
+
|
|
47
|
+
data_copy = {"tabs": {}, "active_tab": self._data["active_tab"]}
|
|
48
|
+
for name, tab in self._data["tabs"].items():
|
|
49
|
+
tab_copy = tab.copy()
|
|
50
|
+
if "component" in tab_copy:
|
|
51
|
+
comp = tab_copy["component"]
|
|
52
|
+
# Resolve type key from registry if not already set
|
|
53
|
+
if "type" not in tab_copy or tab_copy["type"] == "unknown":
|
|
54
|
+
from .registry import get_registry
|
|
55
|
+
|
|
56
|
+
cls = type(comp)
|
|
57
|
+
for key, info in get_registry().items():
|
|
58
|
+
if info["cls"] is cls:
|
|
59
|
+
tab_copy["type"] = key
|
|
60
|
+
break
|
|
61
|
+
tab_copy["data"] = comp.data
|
|
62
|
+
tab_copy["settings"] = snapshot(comp)
|
|
63
|
+
del tab_copy["component"]
|
|
64
|
+
data_copy["tabs"][name] = tab_copy
|
|
65
|
+
return data_copy
|
|
66
|
+
|
|
67
|
+
def add_tab(self, title: str, cls: type, *args, **kwargs):
|
|
68
|
+
name = title.lower().replace(" ", "_")
|
|
69
|
+
# Resolve type key and icon from registry
|
|
70
|
+
from .registry import get_registry
|
|
71
|
+
|
|
72
|
+
type_key = "unknown"
|
|
73
|
+
icon = "mdi-vector-triangle"
|
|
74
|
+
for key, info in get_registry().items():
|
|
75
|
+
if info["cls"] is cls:
|
|
76
|
+
type_key = key
|
|
77
|
+
icon = info["icon"]
|
|
78
|
+
break
|
|
79
|
+
self._data["tabs"][name] = {
|
|
80
|
+
"type": type_key,
|
|
81
|
+
"icon": icon,
|
|
82
|
+
"data": {},
|
|
83
|
+
"name": name,
|
|
84
|
+
"title": title,
|
|
85
|
+
"settings": {},
|
|
86
|
+
}
|
|
87
|
+
component = cls(name, *args, **kwargs)
|
|
88
|
+
self._data["tabs"][name]["component"] = component
|
|
89
|
+
self.active_tab = name
|
|
90
|
+
if self._update is not None:
|
|
91
|
+
self._update()
|
|
92
|
+
return component
|
|
93
|
+
|
|
94
|
+
def get_tabs(self):
|
|
95
|
+
"""
|
|
96
|
+
Get the tabs stored in the AppData instance.
|
|
97
|
+
|
|
98
|
+
:return: A dictionary of tabs.
|
|
99
|
+
"""
|
|
100
|
+
return self._data["tabs"]
|
|
101
|
+
|
|
102
|
+
def get_tab(self, name):
|
|
103
|
+
return self._data["tabs"].get(name, None)
|
|
104
|
+
|
|
105
|
+
def delete_tab(self, name):
|
|
106
|
+
"""
|
|
107
|
+
Delete a tab by name.
|
|
108
|
+
|
|
109
|
+
:param name: The name of the tab to delete.
|
|
110
|
+
"""
|
|
111
|
+
if name in self._data["tabs"]:
|
|
112
|
+
del self._data["tabs"][name]
|
|
113
|
+
if self.active_tab == name:
|
|
114
|
+
self.active_tab = (
|
|
115
|
+
list(self._data["tabs"].keys())[0] if self._data["tabs"] else None
|
|
116
|
+
)
|
|
117
|
+
if self._update is not None:
|
|
118
|
+
self._update()
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def active_tab(self):
|
|
122
|
+
"""
|
|
123
|
+
Get the currently active tab name.
|
|
124
|
+
"""
|
|
125
|
+
return self._data["active_tab"]
|
|
126
|
+
|
|
127
|
+
@active_tab.setter
|
|
128
|
+
def active_tab(self, name):
|
|
129
|
+
"""
|
|
130
|
+
Set the currently active tab by name.
|
|
131
|
+
|
|
132
|
+
:param name: The name of the tab to set as active.
|
|
133
|
+
"""
|
|
134
|
+
self._data["active_tab"] = name
|
ngsolve_gui/appconfig.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ngapp import AppConfig
|
|
2
|
+
|
|
3
|
+
from . import __version__
|
|
4
|
+
from .app import NGSolveGui
|
|
5
|
+
|
|
6
|
+
_DESCRIPTION = """A short description"""
|
|
7
|
+
|
|
8
|
+
config = AppConfig(
|
|
9
|
+
name="NGSolve GUI",
|
|
10
|
+
version=__version__,
|
|
11
|
+
python_class=NGSolveGui,
|
|
12
|
+
frontend_pip_dependencies=["ngsolve", "ngsolve_webgpu"],
|
|
13
|
+
description=_DESCRIPTION,
|
|
14
|
+
)
|