Created
September 3, 2025 18:17
-
-
Save sjgallagher2/20e0aeae38b5a20870460e7cd29cd2ea to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Created on Wed Sep 3 12:41:04 2025 | |
| @author: sam | |
| """ | |
| import sys | |
| import os | |
| import time | |
| import re | |
| import gmsh | |
| os.environ["QT_API"] = "pyside6" | |
| from qtpy import QtWidgets,QtCore | |
| from pyvistaqt import QtInteractor, MainWindow | |
| from pathlib import Path | |
| from emerge._emerge.plot.pyvista import PVBackgroundDisplay | |
| class EmergeViewerMainWindow(MainWindow): | |
| def __init__(self, parent=None, show=True): | |
| QtWidgets.QMainWindow.__init__(self, parent) | |
| self.fname = '' | |
| self.fs_watcher = QtCore.QFileSystemWatcher([self.fname]) | |
| self._cached_geom_str = '' | |
| # create the frame | |
| self.frame = QtWidgets.QFrame() | |
| vlayout = QtWidgets.QVBoxLayout() | |
| # add the pyvista interactor object | |
| self.plotter = QtInteractor(self.frame) | |
| vlayout.addWidget(self.plotter.interactor) | |
| self.signal_close.connect(self.plotter.close) | |
| self.frame.setLayout(vlayout) | |
| self.setCentralWidget(self.frame) | |
| # simple menu to demo functions | |
| main_menu = self.menuBar() | |
| file_menu = main_menu.addMenu('File') | |
| open_button = QtWidgets.QAction('Open', self) | |
| open_button.setShortcut('Ctrl+O') | |
| open_button.triggered.connect(self.file_open_dlg) | |
| file_menu.addAction(open_button) | |
| exit_button = QtWidgets.QAction('Exit', self) | |
| exit_button.setShortcut('Ctrl+Q') | |
| exit_button.triggered.connect(self.close) | |
| file_menu.addAction(exit_button) | |
| mesh_menu = main_menu.addMenu('Geometry') | |
| self.recompile_action = QtWidgets.QAction('Recompile', self) | |
| self.recompile_action.triggered.connect(self.recompile) | |
| mesh_menu.addAction(self.recompile_action) | |
| self.fs_watcher.fileChanged.connect(self.recompile) | |
| if show: | |
| self.show() | |
| self.update_geom_lines() | |
| self.execute_file() | |
| self.cam_pos_saved = self.plotter.camera.position | |
| self.cam_foc_saved = self.plotter.camera.focal_point | |
| self.cam_up_saved = self.plotter.camera.up | |
| self.last_update_time = time.time() | |
| def file_open_dlg(self): | |
| fname_sel = QtWidgets.QFileDialog.getOpenFileName(self, "Open Image", str(Path.home()), "Python Files (*.py)") | |
| fname_sel = fname_sel[0] | |
| if fname_sel != '': | |
| self.fname = fname_sel | |
| self.fs_watcher.removePaths(self.fs_watcher.files()) | |
| self.fs_watcher.addPath(self.fname) | |
| self.recompile() | |
| @staticmethod | |
| def _get_model_name(geom_lines): | |
| if geom_lines is not None: | |
| # Regex for finding an identifier | |
| pattern = re.compile( | |
| r"""^ | |
| ([A-Za-z_][A-Za-z0-9_\.]*) # valid Python identifier | |
| \s*=\s* # equals sign with optional spaces | |
| [A-Za-z_][A-Za-z0-9_]*\.Simulation(.*) # valid identifier (package name) then Simulation(...) | |
| $""", | |
| re.VERBOSE,) | |
| model_varname = '' | |
| for i,line in enumerate(geom_lines): | |
| if line != '\n': | |
| # print(f'{line[:-1]} -> {bool(pattern.match(line))}') | |
| match = pattern.match(line) | |
| if bool(match): | |
| # This is a variable assignment, check if it's for the Simulation object | |
| model_varname = match.group(1) | |
| if model_varname != '': | |
| return model_varname,i # Only first model is returned | |
| else: | |
| # Might be a with...as statement. Try to detect it. | |
| pattern = re.compile( | |
| r""" | |
| with\s[A-Za-z_][A-Za-z0-9_]*\.Simulation(.*) | |
| \sas\s | |
| ([A-Za-z_][A-Za-z0-9_\.]*).*$""", | |
| re.VERBOSE,) | |
| for i,line in enumerate(geom_lines): | |
| if line != '\n': | |
| # print(f'{line[:-1]} -> {bool(pattern.match(line))}') | |
| match = pattern.match(line.strip()) | |
| if bool(match): | |
| # This is a variable assignment, check if it's for the Simulation object | |
| model_varname = match.group(2) | |
| break | |
| if model_varname != '': | |
| return model_varname,i # Only first model is returned | |
| else: | |
| raise ValueError("No suitable assignment of Simulation object found.") | |
| @staticmethod | |
| def _get_geometry_lines(lines,model_name): | |
| indent = '' | |
| # Look for matching lines | |
| pattern = re.compile(f'{model_name}.commit_geometry(.*).*') | |
| end_idx = -1 | |
| for line_no in range(len(lines)): | |
| if bool(pattern.match(lines[line_no].strip())): | |
| end_idx = line_no + 1 # Include this line | |
| indent = lines[line_no][ :lines[line_no].find(model_name) ] | |
| if end_idx == -1: | |
| # Might be implicitly calling commit_geometry() inside generate_mesh() | |
| pattern = re.compile(f'{model_name}.generate_mesh().*') | |
| end_idx = -1 | |
| for line_no in range(len(lines)): | |
| if bool(pattern.match(lines[line_no].strip())): | |
| end_idx = line_no # Exclude this line | |
| indent = lines[line_no][ :lines[line_no].find(model_name) ] | |
| if end_idx == -1: | |
| raise ValueError(f"commit_geometry() command not found with model `{model_name}`.") | |
| geom_lines = lines[:end_idx] | |
| geom_lines = [gl for gl in geom_lines if gl[0] != '#'] | |
| # geom_lines = EmergeViewerMainWindow._join_continued_lines(geom_lines) | |
| return geom_lines,indent # Return first match to commit_geometry() | |
| def recompile(self): | |
| # Recompile, with debounce for editors that delete and resave a file | |
| if self.fname == '': | |
| return | |
| current_time = time.time() | |
| if current_time - self.last_update_time > 1: | |
| did_update = self.update_geom_lines() | |
| if did_update: | |
| # Save camera state | |
| self.cam_pos_saved = self.plotter.camera.position | |
| self.cam_foc_saved = self.plotter.camera.focal_point | |
| self.cam_up_saved = self.plotter.camera.up | |
| # Clear | |
| self.plotter.clear_actors() | |
| # Run geometry scripts | |
| self.execute_file() | |
| # Restore camera state | |
| self.plotter.camera.position = self.cam_pos_saved | |
| self.plotter.camera.focal_point = self.cam_foc_saved | |
| self.plotter.camera.up = self.cam_up_saved | |
| self.last_update_time = current_time | |
| def update_geom_lines(self) -> bool: | |
| """Read file and update cached geometry text, return True if updated, False if no change""" | |
| if self.fname == '': | |
| return False | |
| with open(self.fname, 'r') as f: | |
| lines = f.readlines() | |
| model_name,model_def_line = EmergeViewerMainWindow._get_model_name(lines) | |
| if model_name == '': | |
| raise ValueError("Could not find suitable model name definition.") | |
| geom_lines,indent = EmergeViewerMainWindow._get_geometry_lines(lines,model_name) | |
| # Post-process geometry lines | |
| # Add view update | |
| geom_str = ''.join(geom_lines) | |
| # print(geom_str) | |
| view_update_s = f"""{indent}{model_name}.display = PVBackgroundDisplay({model_name}.mesh, self.plotter) | |
| {indent}try: | |
| {indent} gmsh.model.occ.synchronize() # this is global | |
| {indent} gmsh.model.mesh.generate(3) | |
| {indent} {model_name}.mesh.update() | |
| {indent} {model_name}.mesh.exterior_face_tags = {model_name}.mesher.domain_boundary_face_tags | |
| {indent} gmsh.model.occ.synchronize() | |
| {indent} {model_name}._set_mesh({model_name}.mesh) | |
| {indent}except Exception: | |
| {indent} print("GMSH Mesh error. Using generate_mesh()...") | |
| {indent} {model_name}.generate_mesh() | |
| {indent} print("Success.") | |
| {indent}for geo in {model_name}.data.sim['geometries']: | |
| {indent} # display is a PVBackgroundDisplay object | |
| {indent} # This is initialized with the Mesh3D object model.mesh | |
| {indent} {model_name}.display.add_object(geo, opacity=0.5) | |
| """ | |
| geom_str += view_update_s | |
| # print(geom_str) | |
| if geom_str != self._cached_geom_str: | |
| self._cached_geom_str = geom_str | |
| return True | |
| else: | |
| return False | |
| def execute_file(self): | |
| exec(self._cached_geom_str) | |
| # pass | |
| if __name__ == '__main__': | |
| app = QtWidgets.QApplication(sys.argv) | |
| window = EmergeViewerMainWindow() | |
| sys.exit(app.exec_()) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment