From 871db10edfb18e0de8879e1dcfc1a3ce5e1eb100 Mon Sep 17 00:00:00 2001 From: Lasse Date: Mon, 10 Nov 2025 22:15:06 +0100 Subject: [PATCH 1/5] Add include files pop up --- src/fourc_webviewer/fourc_webserver.py | 236 ++++++++++++++++++++----- src/fourc_webviewer/gui_utils.py | 63 ++++++- 2 files changed, 254 insertions(+), 45 deletions(-) diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index dca8cc0..e759d21 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -68,6 +68,10 @@ def __init__( self.server = get_server() + # initialize include upload value: False (bottom sheet with include upload is not displayed until there is a fourcyaml file uploaded) + self.state.include_upload_open = False + self.state.included_files = [] + # declare server-side variable dict: variables which should not # be exposed to the client-side self._server_vars = {} @@ -120,18 +124,11 @@ def __init__( # initialize state object self.init_state_and_server_vars() - # convert file to vtu and create dedicated render objects - fourc_geometry = FourCGeometry( - fourc_yaml_file=fourc_yaml_file, - temp_dir=Path(self._server_vars["temp_dir_object"].name), - ) - self.state.vtu_path = fourc_geometry.vtu_file_path + if "render_window" not in self._actors: + self._server_vars["render_window"] = pv.Plotter() + self.state.vtu_path = "" - if self.state.vtu_path == "": - self.state.read_in_status = self.state.all_read_in_statuses[ - "vtu_conversion_error" - ] - self.init_pyvista_render_objects() + self._server_vars["fourc_yaml_file_dir"] = Path(fourc_yaml_file).parent # create ui create_gui(self.server, self._server_vars["render_window"]) @@ -267,11 +264,57 @@ def init_pyvista_render_objects(self): The saved vtu file path is hereby utilized. """ - if "render_window" not in self._actors: - self._server_vars["render_window"] = pv.Plotter() + # convert file to vtu and create dedicated render objects + if not Path( + self._server_vars["temp_dir_object"].name + + "\\" + + self._server_vars["fourc_yaml_name"] + ).exists(): + raise Exception( + "File does not exist: " + + self._server_vars["temp_dir_object"].name + + "\\" + + self._server_vars["fourc_yaml_name"] + ) + geometry_file_name = ( + self._server_vars["fourc_yaml_content"] + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE") + ) + if geometry_file_name: + if not Path( + self._server_vars["temp_dir_object"].name + + "\\" + + Path( + self._server_vars["fourc_yaml_content"] + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE") + ).name + ).exists(): + raise Exception( + "File does not exist: " + + self._server_vars["temp_dir_object"].name + + "\\" + + Path( + self._server_vars["fourc_yaml_content"]["STRUCTURE GEOMETRY"][ + "FILE" + ] + ).name + ) + fourc_geometry = FourCGeometry( + fourc_yaml_file=self._server_vars["temp_dir_object"].name + + "\\" + + self._server_vars["fourc_yaml_name"], + temp_dir=Path(self._server_vars["temp_dir_object"].name), + ) + self.state.vtu_path = fourc_geometry.vtu_file_path - self._server_vars["render_window"].clear_actors() + if self.state.vtu_path == "": + self.state.read_in_status = self.state.all_read_in_statuses[ + "vtu_conversion_error" + ] + self._server_vars["render_window"].clear_actors() problem_mesh = pv.read(self.state.vtu_path) # get problem mesh self._actors["problem_mesh"] = self._server_vars["render_window"].add_mesh( @@ -334,6 +377,12 @@ def init_pyvista_render_objects(self): all_result_descriptions = self.state.result_description_section.keys() for dc in all_result_descriptions: + if ( + not self.state.result_description_section[dc] + .get("PARAMETERS", {}) + .get("NODE") + ): + continue node_coords = problem_mesh.points[ self.state.result_description_section[dc]["PARAMETERS"]["NODE"] - 1, :, @@ -351,6 +400,8 @@ def init_pyvista_render_objects(self): ) self.update_pyvista_render_objects() + self._server_vars["render_window"].reset_camera() + def update_pyvista_render_objects(self): """Update/ initialize pyvista view objects (reader, thresholds, global COS, ...) for the rendered window. @@ -359,7 +410,7 @@ def update_pyvista_render_objects(self): """ legend_items = [] - for dc in self._actors["result_description_nodes"].values(): + for dc in self._actors.get("result_description_nodes", {}).values(): dc.SetVisibility(False) if ( self.state.selected_main_section_name == "RESULT DESCRIPTION" @@ -372,7 +423,7 @@ def update_pyvista_render_objects(self): ].SetVisibility(True) legend_items.append(("Selected result description", "deepskyblue")) - for rd in self._actors["dc_geometry_entities"].values(): + for rd in self._actors.get("dc_geometry_entities", {}).values(): rd.SetVisibility(False) if ( self.state.selected_main_section_name == "DESIGN CONDITIONS" @@ -384,7 +435,7 @@ def update_pyvista_render_objects(self): ].SetVisibility(True) legend_items.append(("Selected design condition", "navy")) - for mat in self._actors["material_meshes"].values(): + for mat in self._actors.get("material_meshes", {}).values(): mat.SetVisibility(False) if ( self.state.selected_material @@ -743,7 +794,9 @@ def init_result_description_state_and_server_vars(self): # get result description section result_description_section = copy.deepcopy( - self._server_vars["fourc_yaml_content"]["RESULT DESCRIPTION"] + self._server_vars["fourc_yaml_content"].sections.get( + "RESULT DESCRIPTION", {} + ) ) # initialize empty dict as the result description section @@ -913,6 +966,10 @@ def init_funct_state_and_server_vars(self): all_contained_var_names = get_variable_names_in_funct_expression( item_data["SYMBOLIC_FUNCTION_OF_SPACE_TIME"] ) + if "e" in all_contained_var_names: + all_contained_var_names.remove("e") + if "E" in all_contained_var_names: + all_contained_var_names.remove("E") # loop through contained variables and see whether they are evaluable for contained_var_name in all_contained_var_names: @@ -954,6 +1011,46 @@ def init_funct_state_and_server_vars(self): 6 # precision for the user input of the values defined above: x, y, z and t_max ) + def request_included_files(self): + """Requests the included files from the user by opening a the include + files dialog and setting up the state variable accordingly.""" + included_files = [] + + exo_file_name = Path( + self._server_vars.get("fourc_yaml_content") + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE") + or "" + ).name + if exo_file_name: + exo_file_server = Path( + self._server_vars["fourc_yaml_file_dir"], + exo_file_name, + ) + exo_temp_path = Path( + self._server_vars["temp_dir_object"].name, + exo_file_name, + ) + if exo_file_server.is_file(): + with open(exo_file_server, "rb") as fr: + with open(exo_temp_path, "wb") as fw: + fw.write(fr.read()) + elif not exo_temp_path.is_file(): + included_files.append( + { + "name": exo_file_name, + "uploaded": False, + "error": None, + "content": None, + } + ) + + self.state.included_files = included_files + if self.state.included_files: + self.state.include_upload_open = True + else: + self.confirm_included_files() + def sync_funct_section_from_state(self): """Syncs the server-side functions section based on the current values of the dedicated state variables.""" @@ -1025,12 +1122,22 @@ def init_mode_state_vars(self): def change_fourc_yaml_file(self, fourc_yaml_file, **kwargs): """Reaction to change of state.fourc_yaml_file.""" + if not fourc_yaml_file or fourc_yaml_file["name"].split(".")[-1] not in [ + "yaml", + "yml", + "DAT", + "dat", + ]: + print( + "Warning: File does not have a .yml / .yaml / .dat / .DAT ending or is empty. Try opening another file." + ) + return # create temporary fourc yaml file from the content of the given file temp_fourc_yaml_file = Path( self._server_vars["temp_dir_object"].name, fourc_yaml_file["name"] ) - with open(temp_fourc_yaml_file, "w") as f: - f.writelines(fourc_yaml_file["content"].decode("utf-8")) + with open(temp_fourc_yaml_file, "wb") as f: + f.write(fourc_yaml_file["content"]) # read content, lines and other details of the given file ( @@ -1042,12 +1149,63 @@ def change_fourc_yaml_file(self, fourc_yaml_file, **kwargs): ) = read_fourc_yaml_file(temp_fourc_yaml_file) self._server_vars["fourc_yaml_name"] = Path(temp_fourc_yaml_file).name - # set vtu file path empty to make the convert button visible - # (only if the function was not run yet, i.e., after the - # initial rendering) - self._server_vars["render_count"]["change_fourc_yaml_file"] += 1 - if self._server_vars["render_count"]["change_fourc_yaml_file"] > 1: - self.state.vtu_path = "" + if self._server_vars["fourc_yaml_read_in_status"]: + self.state.read_in_status = self.state.all_read_in_statuses["success"] + else: + self.state.read_in_status = self.state.all_read_in_statuses[ + "validation_error" + ] + + self._server_vars["fourc_yaml_name"] = temp_fourc_yaml_file.name + + self.request_included_files() + + @controller.set("on_upload_include_file") + def on_upload_include_file(self, uploaded_file, index, **kwargs): + """Gets called when an included file is uploaded. + + Saves the uploaded file into the state variable. + """ + self.state.included_files[index]["content"] = uploaded_file + + try: + if self.state.included_files[index]["name"] != uploaded_file["name"]: + self.state.included_files[index]["error"] = ( + "File name mismatch. Expected: " + + self.state.included_files[index]["name"] + ) + elif self.state.included_files[index]["content"]["size"] == 0: + self.state.included_files[index]["error"] = "File is empty." + else: + self.state.included_files[index]["error"] = None + self.state.included_files[index]["uploaded"] = True + except Exception: + self.state.included_files[index]["error"] = "Please upload a file." + self.state.included_files[index]["uploaded"] = False + self.state.dirty("included_files") + self.state.flush() + + @controller.set("confirm_included_files") + def confirm_included_files(self, **kwargs): + """Gets called when the Accept button in the included files dialog is + pressed. + + Saves all files into the temporary directory. + """ + self.state.include_upload_open = False + + for included_file in self.state.included_files: + # create file in temp directory + included_file_path = Path( + self._server_vars["temp_dir_object"].name, + included_file["content"]["name"], + ) + with open(included_file_path, "wb") as f: + f.write(included_file["content"]["content"]) + + self.init_state_and_server_vars() + + self.init_pyvista_render_objects() @change("export_fourc_yaml_path") def change_export_fourc_yaml_path(self, export_fourc_yaml_path, **kwargs): @@ -1082,7 +1240,6 @@ def change_selected_material(self, selected_material, **kwargs): # material (if we are not in an initial rendering scenario) if self._server_vars["render_count"]["change_selected_material"] > 0: # first get the master material id - master_mat_id = self.determine_master_mat_ind_for_current_selection() # update plotter / render objects self.update_pyvista_render_objects() @@ -1158,7 +1315,7 @@ def change_selected_funct(self, selected_funct, **kwargs): # set the selected funct item to the first within the newly # selected funct self.state.selected_funct_item = next( - iter(self.state.funct_section[selected_funct]) + iter(self.state.funct_section.get(selected_funct, {})) ) # update plotly figure @@ -1183,18 +1340,22 @@ def change_selected_funct_item(self, selected_funct_item, **kwargs): def change_funct_plot(self, funct_plot, **kwargs): """Reaction to change of state.funct_plot.""" # update plotly figure - if self.state.funct_section[self.state.selected_funct][ - self.state.selected_funct_item - ]["VISUALIZATION"]: + if ( + self.state.funct_section.get(self.state.selected_funct, {}) + .get(self.state.selected_funct_item, {}) + .get("VISUALIZATION") + ): self.server.controller.figure_update(function_plot_figure(self.state)) @change("funct_section") def change_funct_section(self, funct_section, **kwargs): """Reaction to change of state.funct_section.""" # update plotly figure - if self.state.funct_section[self.state.selected_funct][ - self.state.selected_funct_item - ]["VISUALIZATION"]: + if ( + self.state.funct_section.get(self.state.selected_funct, {}) + .get(self.state.selected_funct_item, {}) + .get("VISUALIZATION") + ): self.server.controller.figure_update(function_plot_figure(self.state)) ################################################# @@ -1281,12 +1442,7 @@ def click_convert_button(self, **kwargs): # initialize state object self.init_state_and_server_vars() - # convert to vtu - fourc_geometry = FourCGeometry( - fourc_yaml_file=temp_fourc_yaml_file, - temp_dir=Path(self._server_vars["temp_dir_object"].name), - ) - self.state.vtu_path = fourc_geometry.vtu_file_path + self.init_pyvista_render_objects() # catch eventual conversion error if self.state.vtu_path == "": diff --git a/src/fourc_webviewer/gui_utils.py b/src/fourc_webviewer/gui_utils.py index 41834bd..4b86a45 100644 --- a/src/fourc_webviewer/gui_utils.py +++ b/src/fourc_webviewer/gui_utils.py @@ -143,7 +143,7 @@ def _toolbar(server_controller): vuetify.VFileInput( label="Input file", v_model=("fourc_yaml_file",), - update_modelValue="flushState('fourc_yaml_file')", + # update_modelValue="flushState('fourc_yaml_file')", accept=".yaml,.yml", ) vuetify.VBtn( @@ -242,6 +242,58 @@ def _bottom_sheet_export(server_controller): ) +def _bottom_sheet_include_upload(server): + """Bottom sheet layout (EXPORT mode).""" + + with vuetify.VDialog( + v_model=("include_upload_open",), persistent=True, max_width="600px" + ): + with vuetify.VCard(classes="pa-5"): + vuetify.VCardTitle("Upload Included Files") + + with vuetify.VCardText(): + with vuetify.VRow( + dense=True, + align="center", + v_for="(file, i) in included_files", + key=("included_files[i].name",), + ): + with vuetify.VCol(cols=11): + vuetify.VFileInput( + update_modelValue=( + server.controller.on_upload_include_file, + "[$event, i]", + ), + label=("file.name",), + multiple=False, + variant="outlined", + color=( + "file.error ? 'error' : file.uploaded ? 'success' : undefined", + ), + error_messages=("file.error",), + ) + with vuetify.VCol(cols=1): + vuetify.VIcon( + icon=( + "file.error || !file.uploaded ? 'mdi-alert-circle' : 'mdi-check-circle'", + ), + color=( + "file.error ? 'error' : file.uploaded ? 'success' : 'primary'", + ), + classes="mr-2 pb-5 pl-3", + size="36", + ) + with vuetify.VCardActions(classes="justify-end"): + vuetify.VBtn( + "Accept", + size="large", + color="primary", + disabled=("!included_files.every(f => !f.error && f.uploaded)",), + click=(server.controller.confirm_included_files,), + variant="text", + ) + + def _sections_dropdown(): """Section dropdown layout.""" vuetify.VSelect( @@ -754,8 +806,7 @@ def _prop_value_table(server): "|| json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] == 'integer')" "&& !json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['enum']" ), - blur=server.controller.on_leave_edit_field, - update_modelValue="flushState('general_sections')", # this is required in order to flush the state changes correctly to the server, as our passed on v-model is a nested variable + update_modelValue="flushState('add_value')", # this is required in order to flush the state changes correctly to the server, as our passed on v-model is a nested variable classes="w-80 pb-1", dense=True, # If we will add errors for this later @@ -775,7 +826,7 @@ def _prop_value_table(server): vuetify.VSwitch( v_model=("add_value"), classes="mt-4", - update_modelValue="flushState('general_sections')", + update_modelValue="flushState('add_value')", class_="mx-100", dense=True, color="primary", @@ -787,7 +838,7 @@ def _prop_value_table(server): "json_schema['properties']?.[selected_section_name]" "?.['properties']?.[add_key]?.['enum']" ), - update_modelValue="flushState('general_sections')", + update_modelValue="flushState('add_value')", # bind the enum array as items items=( "json_schema['properties'][selected_section_name]['properties'][add_key]['enum']", @@ -1411,6 +1462,8 @@ def create_gui(server, render_window): _bottom_sheet_info() _bottom_sheet_export(server.controller) + _bottom_sheet_include_upload(server) + with layout.drawer as drawer: drawer.width = 800 with html.Div(v_if=("vtu_path != ''",)): From 918ac2e3e45c10c3b777c29ade820dcc6058ba32 Mon Sep 17 00:00:00 2001 From: Lasse Date: Tue, 11 Nov 2025 11:21:31 +0100 Subject: [PATCH 2/5] Add yaml includes recursive --- src/fourc_webviewer/fourc_webserver.py | 69 ++++++++++++++++++-------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index e759d21..fbe328d 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -12,6 +12,7 @@ import yaml from fourcipp import CONFIG from fourcipp.fourc_input import FourCInput, ValidationError +from fourcipp.utils.yaml_io import load_yaml from trame.app import get_server from trame.decorators import TrameApp, change, controller @@ -1011,41 +1012,58 @@ def init_funct_state_and_server_vars(self): 6 # precision for the user input of the values defined above: x, y, z and t_max ) - def request_included_files(self): - """Requests the included files from the user by opening a the include - files dialog and setting up the state variable accordingly.""" - included_files = [] + def append_include_files(self, file_paths): + """Appends list of files to the included files input field. - exo_file_name = Path( - self._server_vars.get("fourc_yaml_content") - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE") - or "" - ).name - if exo_file_name: - exo_file_server = Path( + They will be uploaded before the user can edit or view the file. + """ + yaml_include_names = [Path(file_path).name for file_path in file_paths] + included_files = copy.deepcopy(self.state.included_files) + for include_name in yaml_include_names: + include_file_server = Path( self._server_vars["fourc_yaml_file_dir"], - exo_file_name, + include_name, ) - exo_temp_path = Path( + include_temp_path = Path( self._server_vars["temp_dir_object"].name, - exo_file_name, + include_name, ) - if exo_file_server.is_file(): - with open(exo_file_server, "rb") as fr: - with open(exo_temp_path, "wb") as fw: + if include_file_server.is_file(): + with open(include_file_server, "rb") as fr: + with open(include_temp_path, "wb") as fw: fw.write(fr.read()) - elif not exo_temp_path.is_file(): + elif not include_temp_path.is_file(): included_files.append( { - "name": exo_file_name, + "name": include_name, "uploaded": False, "error": None, "content": None, } ) - self.state.included_files = included_files + + def request_included_files(self): + """Requests the included files from the user by opening a the include + files dialog and setting up the state variable accordingly.""" + + self.append_include_files( + [ + self._server_vars.get("fourc_yaml_content") + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE") + or "" + ] + ) + # add yaml includes + yaml_include_names = [ + Path(file_path).name + for file_path in self._server_vars.get("fourc_yaml_content").sections.get( + "INCLUDES", [] + ) + ] + self.append_include_files(yaml_include_names) + if self.state.included_files: self.state.include_upload_open = True else: @@ -1168,6 +1186,15 @@ def on_upload_include_file(self, uploaded_file, index, **kwargs): """ self.state.included_files[index]["content"] = uploaded_file + if uploaded_file["name"].split(".")[-1] in ["yaml", "yml"]: + content = ( + load_yaml(uploaded_file.get("content", {}).get("content", "")) or {} + ) + yaml_include_names = [ + Path(file_path).name for file_path in content.get("INCLUDES", []) + ] + self.append_include_files(yaml_include_names) + try: if self.state.included_files[index]["name"] != uploaded_file["name"]: self.state.included_files[index]["error"] = ( From 6109f1a70c15e62f94de31de0c2fc17d13e46b98 Mon Sep 17 00:00:00 2001 From: Lasse Date: Thu, 27 Nov 2025 20:50:17 +0100 Subject: [PATCH 3/5] improve readability and fixes --- src/fourc_webviewer/fourc_webserver.py | 98 +++++++++++-------- .../fourc_yaml_file_visualization.py | 4 +- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index 0d39880..5c0f652 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -4,6 +4,7 @@ import copy import re +import shutil import tempfile from pathlib import Path @@ -125,7 +126,7 @@ def __init__( # initialize state object self.init_state_and_server_vars() - if "render_window" not in self._actors: + if "render_window" not in self._server_vars: self._server_vars["render_window"] = pv.Plotter() self.state.vtu_path = "" @@ -266,46 +267,43 @@ def init_pyvista_render_objects(self): The saved vtu file path is hereby utilized. """ # convert file to vtu and create dedicated render objects - if not Path( - self._server_vars["temp_dir_object"].name - + "\\" - + self._server_vars["fourc_yaml_name"] + if not ( + Path(self._server_vars["temp_dir_object"].name) + / self._server_vars["fourc_yaml_name"] ).exists(): raise Exception( "File does not exist: " + self._server_vars["temp_dir_object"].name - + "\\" + + "/" + self._server_vars["fourc_yaml_name"] ) - geometry_file_name = ( - self._server_vars["fourc_yaml_content"] - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE") - ) + + # contains the dict of the structure geometry section of the current yaml file. + structure_geometry_section = self._server_vars[ + "fourc_yaml_content" + ].sections.get("STRUCTURE GEOMETRY", {}) + # contains the name of the geometry file defined in the STRUCTURE GEOMETRY section. + geometry_file_name = structure_geometry_section.get("FILE", None) + if geometry_file_name: - if not Path( - self._server_vars["temp_dir_object"].name - + "\\" - + Path( - self._server_vars["fourc_yaml_content"] - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE") - ).name + # ensure that geometry_file_name really only contains the name and not a path + geometry_file_name = Path(geometry_file_name).name + if not ( + Path(self._server_vars["temp_dir_object"].name) / geometry_file_name ).exists(): + # if the current yaml file references a geometry file it will have already been loaded into the temp dir by now. + # if not something went wrong raise Exception( "File does not exist: " + self._server_vars["temp_dir_object"].name - + "\\" - + Path( - self._server_vars["fourc_yaml_content"]["STRUCTURE GEOMETRY"][ - "FILE" - ] - ).name + + "/" + + geometry_file_name ) + + # creates the FourCGeometry. By now every used file has to be in the temp dir fourc_geometry = FourCGeometry( - fourc_yaml_file=self._server_vars["temp_dir_object"].name - + "\\" - + self._server_vars["fourc_yaml_name"], + fourc_yaml_file=Path(self._server_vars["temp_dir_object"].name) + / self._server_vars["fourc_yaml_name"], temp_dir=Path(self._server_vars["temp_dir_object"].name), ) self.state.vtu_path = fourc_geometry.vtu_file_path @@ -972,10 +970,6 @@ def init_funct_state_and_server_vars(self): all_contained_var_names = get_variable_names_in_funct_expression( item_data["SYMBOLIC_FUNCTION_OF_SPACE_TIME"] ) - if "e" in all_contained_var_names: - all_contained_var_names.remove("e") - if "E" in all_contained_var_names: - all_contained_var_names.remove("E") # loop through contained variables and see whether they are evaluable for contained_var_name in all_contained_var_names: @@ -1022,22 +1016,32 @@ def append_include_files(self, file_paths): They will be uploaded before the user can edit or view the file. """ + # get the file names of the needed files. These names will be shown in the pop up window. yaml_include_names = [Path(file_path).name for file_path in file_paths] included_files = copy.deepcopy(self.state.included_files) + # make a copy, so the state triggers reactivity for include_name in yaml_include_names: + # this file path is created to check weather the needed file is already present on the server. + # If the user is, for example, continuously working on a file that references an exodus file + # they can copy it into the server file directory and it will be opened automatically + # without prompting the user to upload the .exo file every time. include_file_server = Path( self._server_vars["fourc_yaml_file_dir"], include_name, ) + # every file the user is working on will be loaded into the temp directory. + # This is because the FourCGeometry Constructor requires the .yaml file and the .exo file to be in the same directory. include_temp_path = Path( self._server_vars["temp_dir_object"].name, include_name, ) + + # if the file has been copied into the server directory it will be loaded into the temp dir automatically + # without prompting the user every time they open the .yaml file. if include_file_server.is_file(): - with open(include_file_server, "rb") as fr: - with open(include_temp_path, "wb") as fw: - fw.write(fr.read()) + shutil.copyfile(include_file_server, include_temp_path) elif not include_temp_path.is_file(): + # This is the standard case. The file is not present on the server and the user is prompted to upload it. included_files.append( { "name": include_name, @@ -1046,20 +1050,24 @@ def append_include_files(self, file_paths): "content": None, } ) + # trigger reactivity self.state.included_files = included_files def request_included_files(self): """Requests the included files from the user by opening a the include files dialog and setting up the state variable accordingly.""" - self.append_include_files( - [ - self._server_vars.get("fourc_yaml_content") - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE") - or "" - ] + self.state.included_files = [] + # if the uploaded .yaml file contains a reference to a geometry file, this variable will be it's name. + # otherwise it will be None + geometry_file_name = ( + self._server_vars.get("fourc_yaml_content") + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE", None) ) + + if geometry_file_name: + self.append_include_files([geometry_file_name]) # add yaml includes yaml_include_names = [ Path(file_path).name @@ -1435,10 +1443,14 @@ def change_selected_result_description_id( @change("selected_funct") def change_selected_funct(self, selected_funct, **kwargs): """Reaction to change of state.selected_funct.""" + # if there is no function_section + if not self.state.funct_section.get(selected_funct, {}): + return + # set the selected funct item to the first within the newly # selected funct self.state.selected_funct_item = next( - iter(self.state.funct_section.get(selected_funct, {})) + iter(self.state.funct_section.get(selected_funct, {})), ) # update plotly figure diff --git a/src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py b/src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py index 046e180..6e90a57 100644 --- a/src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py +++ b/src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py @@ -22,7 +22,9 @@ def get_variable_names_in_funct_expression(funct_expression: str): regular expressions.""" vars_found = re.findall(r"[A-Za-z_]+", funct_expression) return [ - v for v in vars_found if v not in DEF_FUNCT and v not in ["t", "x", "y", "z"] + v + for v in vars_found + if v not in DEF_FUNCT and v not in ["t", "x", "y", "z", "e", "E"] ] From 901dfc782b5d45694f5726e779bca3958d2bf8bb Mon Sep 17 00:00:00 2001 From: Lasse Date: Tue, 16 Dec 2025 11:30:38 +0100 Subject: [PATCH 4/5] fix and clean up --- src/fourc_webviewer/fourc_webserver.py | 74 +++++++------------------- src/fourc_webviewer/gui_utils.py | 5 -- 2 files changed, 19 insertions(+), 60 deletions(-) diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index 5c0f652..6b80f3a 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -14,6 +14,7 @@ from fourcipp import CONFIG from fourcipp.fourc_input import FourCInput, ValidationError from fourcipp.utils.yaml_io import load_yaml +from loguru import logger from trame.app import get_server from trame.decorators import TrameApp, change, controller @@ -39,6 +40,7 @@ ) from fourc_webviewer.read_geometry_from_file import ( FourCGeometry, + check_for_geometry_files, ) # Global variable @@ -48,6 +50,8 @@ # always set pyvista to plot off screen with Trame pv.OFF_SCREEN = True +pv.set_error_output_file("vtk_errors.log") + @TrameApp() class FourCWebServer: @@ -130,6 +134,8 @@ def __init__( self._server_vars["render_window"] = pv.Plotter() self.state.vtu_path = "" + # self._server_vars["fourc_yaml_file_dir"] is a Path to the parent directory of the initial file. + # This is not always connected to the currently open file. self._server_vars["fourc_yaml_file_dir"] = Path(fourc_yaml_file).parent # create ui @@ -278,12 +284,9 @@ def init_pyvista_render_objects(self): + self._server_vars["fourc_yaml_name"] ) - # contains the dict of the structure geometry section of the current yaml file. - structure_geometry_section = self._server_vars[ - "fourc_yaml_content" - ].sections.get("STRUCTURE GEOMETRY", {}) - # contains the name of the geometry file defined in the STRUCTURE GEOMETRY section. - geometry_file_name = structure_geometry_section.get("FILE", None) + geometry_file_name = check_for_geometry_files( + self._server_vars["fourc_yaml_content"] + )[0] if geometry_file_name: # ensure that geometry_file_name really only contains the name and not a path @@ -319,9 +322,11 @@ def init_pyvista_render_objects(self): self._server_vars["render_window"].clear_actors() problem_mesh = pv.read(self.state.vtu_path) - # get problem mesh - self._actors["problem_mesh"] = self._server_vars["render_window"].add_mesh( - problem_mesh, color="bisque", opacity=0.2, render=False + # get problem mesh. add_mesh returns an actor, so we have to get the actors mesh + self._actors["problem_mesh"] = ( + self._server_vars["render_window"] + .add_mesh(problem_mesh, color="bisque", opacity=0.2, render=False) + .mapper.dataset ) # get mesh of the selected material @@ -1060,11 +1065,9 @@ def request_included_files(self): self.state.included_files = [] # if the uploaded .yaml file contains a reference to a geometry file, this variable will be it's name. # otherwise it will be None - geometry_file_name = ( - self._server_vars.get("fourc_yaml_content") - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE", None) - ) + geometry_file_name = check_for_geometry_files( + self._server_vars["fourc_yaml_content"] + )[0] if geometry_file_name: self.append_include_files([geometry_file_name]) @@ -1159,8 +1162,8 @@ def change_fourc_yaml_file(self, fourc_yaml_file, **kwargs): "DAT", "dat", ]: - print( - "Warning: File does not have a .yml / .yaml / .dat / .DAT ending or is empty. Try opening another file." + logger.warning( + "File does not have a .yml / .yaml / .dat / .DAT ending or is empty. Try opening another file." ) return # create temporary fourc yaml file from the content of the given file @@ -1557,45 +1560,6 @@ def click_export_button(self, **kwargs): settings.""" self.state.export_mode = not self.state.export_mode - @controller.set("click_convert_button") - def click_convert_button(self, **kwargs): - """Convert the given fourc yaml file to vtu and run the state - initialization routines.""" - - # create temporary fourc yaml file from the content of the given file - temp_fourc_yaml_file = Path( - self._server_vars["temp_dir_object"].name, - self.state.fourc_yaml_file["name"], - ) - - with open(temp_fourc_yaml_file, "w") as f: - f.write(self.state.fourc_yaml_file["content"].decode("utf-8")) - - if self._server_vars["fourc_yaml_read_in_status"]: - self.state.read_in_status = self.state.all_read_in_statuses["success"] - - # initialize state object - self.init_state_and_server_vars() - - self.init_pyvista_render_objects() - - # catch eventual conversion error - if self.state.vtu_path == "": - self.state.read_in_status = self.state.all_read_in_statuses[ - "vtu_conversion_error" - ] - else: - # reset view - self.init_pyvista_render_objects() - self._server_vars["render_window"].reset_camera() - self.ctrl.view_reset_camera() - self.ctrl.view_update() - - else: - self.state.read_in_status = self.state.all_read_in_statuses[ - "validation_error" - ] - @controller.set("click_save_button") def click_save_button(self, **kwargs): """Save the current content to a new fourc_yaml content.""" diff --git a/src/fourc_webviewer/gui_utils.py b/src/fourc_webviewer/gui_utils.py index 7cd2a2f..b72c0e0 100644 --- a/src/fourc_webviewer/gui_utils.py +++ b/src/fourc_webviewer/gui_utils.py @@ -146,11 +146,6 @@ def _toolbar(server_controller): # update_modelValue="flushState('fourc_yaml_file')", accept=".yaml,.yml", ) - vuetify.VBtn( - text="CONVERT", - v_if=("vtu_path == ''",), - click=server_controller.click_convert_button, - ) vuetify.VBtn( text="INFO", outlined=True, From c869169f358c814b736357f7e1e9591c7d6dd05a Mon Sep 17 00:00:00 2001 From: Lasse Date: Thu, 18 Dec 2025 16:22:39 +0100 Subject: [PATCH 5/5] change base dir for resolve to temp_dir --- src/fourc_webviewer/read_geometry_from_file.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/fourc_webviewer/read_geometry_from_file.py b/src/fourc_webviewer/read_geometry_from_file.py index 0c2535d..8930da0 100644 --- a/src/fourc_webviewer/read_geometry_from_file.py +++ b/src/fourc_webviewer/read_geometry_from_file.py @@ -513,9 +513,14 @@ def __init__( elif self.geom_type == "external_geometry": try: # read mesh: for the first rendering, we take the relative path with respect to the yaml file; for subsequent renderings, we will account for the absolute path - self._mesh_file = temp_dir / Path( + self._mesh_file = Path( get_geometry_file(fourc_yaml=self._fourc_yaml)[0] ) + if first_render: + self._mesh_file = Path(fourc_yaml_file).parent / self._mesh_file + else: + self._mesh_file = (temp_dir / self._mesh_file).resolve() + if not self._mesh_file.exists(): raise Exception( f"The mesh file {self._mesh_file} does not exist for the fourc yaml file {fourc_yaml_file}"