From 1272be317bf375fbaefb391511710d8aa8502fb4 Mon Sep 17 00:00:00 2001 From: marioalexis Date: Sun, 15 Sep 2024 21:03:17 -0300 Subject: [PATCH 1/2] Fem: Add base class for FemMesh task panels --- src/Mod/Fem/CMakeLists.txt | 1 + .../femtaskpanels/base_femmeshtaskpanel.py | 188 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index b793c570c7..9a53d41f94 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -571,6 +571,7 @@ SET(FemGuiObjects_SRCS SET(FemGuiTaskPanels_SRCS femtaskpanels/__init__.py femtaskpanels/base_femtaskpanel.py + femtaskpanels/base_femmeshtaskpanel.py femtaskpanels/task_constraint_bodyheatsource.py femtaskpanels/task_constraint_centrif.py femtaskpanels/task_constraint_currentdensity.py diff --git a/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py b/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py new file mode 100644 index 0000000000..aa4adb613a --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py @@ -0,0 +1,188 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2024 Mario Passaglia * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM mesh base task panel for mesh object object" +__author__ = "Mario Passaglia" +__url__ = "https://www.freecad.org" + +## @package base_femmeshtaskpanel +# \ingroup FEM +# \brief base task panel for mesh object + +import time +import threading +from abc import ABC, abstractmethod + +from PySide import QtCore +from PySide import QtGui + +import FreeCAD + +from femtools.femutils import getOutputWinColor + +from . import base_femtaskpanel + + +class _Process(threading.Thread): + """ + Class for thread and subprocess manipulation + 'tool' argument must be an object with a 'compute' method + and a 'process' attribute of type Popen object + """ + + def __init__(self, tool): + self.tool = tool + self._timer = QtCore.QTimer() + self.success = False + self.update = False + self.error = "" + super().__init__(target=self.tool.compute) + QtCore.QObject.connect(self._timer, QtCore.SIGNAL("timeout()"), self._check) + + def init(self): + self._timer.start(100) + self.start() + + def run(self): + try: + self.success = self._target(*self._args, **self._kwargs) + except Exception as e: + self.error = str(e) + + def finish(self): + if self.tool.process: + self.tool.process.terminate() + self.join() + + def _check(self): + if not self.is_alive(): + self._timer.stop() + self.join() + if self.success: + try: + self.tool.update_properties() + self.update = True + except Exception as e: + self.error = str(e) + self.success = False + + +class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): + """ + Abstract base class for FemMesh object TaskPanel + """ + + def __init__(self, obj): + super().__init__(obj) + + self.tool = None + self.form = None + self.timer = QtCore.QTimer() + self.process = None + self.console_message = "" + + @abstractmethod + def set_mesh_params(self): + pass + + @abstractmethod + def get_mesh_params(self): + pass + + def getStandardButtons(self): + button_value = ( + QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Cancel + ) + return button_value + + def accept(self): + if self.process and self.process.is_alive(): + FreeCAD.Console.PrintWarning("Process still running\n") + return None + + self.timer.stop() + QtGui.QApplication.restoreOverrideCursor() + self.set_mesh_params() + return super().accept() + + def reject(self): + self.timer.stop() + QtGui.QApplication.restoreOverrideCursor() + if self.process and self.process.is_alive(): + self.console_log("Process aborted", "#ff6700") + self.process.finish() + else: + return super().reject() + + def clicked(self, button): + if button == QtGui.QDialogButtonBox.Apply: + if self.process and self.process.is_alive(): + FreeCAD.Console.PrintWarning("Process already running\n") + return None + + self.set_mesh_params() + self.run_mesher() + + def console_log(self, message="", outputwin_color_type=None): + self.console_message = self.console_message + ( + '{:4.1f}: '.format( + getOutputWinColor("Logging"), time.time() - self.time_start + ) + ) + if outputwin_color_type: + self.console_message += '{}
'.format( + outputwin_color_type, message + ) + else: + self.console_message += message + "
" + self.form.te_output.setText(self.console_message) + self.form.te_output.moveCursor(QtGui.QTextCursor.End) + + def update_timer_text(self): + if self.process and self.process.is_alive(): + self.form.l_time.setText(f"Time: {time.time() - self.time_start:4.1f}: ") + else: + if self.process: + if self.process.success: + if not self.process.update: + return None + self.console_log("Success!", "#00AA00") + else: + self.console_log(self.process.error, "#AA0000") + self.timer.stop() + QtGui.QApplication.restoreOverrideCursor() + + def run_mesher(self): + self.process = _Process(self.tool) + self.timer.start(100) + self.time_start = time.time() + self.form.l_time.setText(f"Time: {time.time() - self.time_start:4.1f}: ") + self.console_message = "" + self.console_log("Start process...") + QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + + self.process.init() + + def get_version(self): + full_message = self.tool.version() + QtGui.QMessageBox.information(None, "{} - Information".format(self.tool.name), full_message) From 0a8ba606202601db4b2da551ea7658e4aba8f9fe Mon Sep 17 00:00:00 2001 From: marioalexis Date: Sun, 15 Sep 2024 21:04:41 -0300 Subject: [PATCH 2/2] Fem: Enable cancel meshing for Gmsh - fixes #5914 --- src/Mod/Fem/Gui/Resources/ui/MeshGmsh.ui | 108 ++++++------ src/Mod/Fem/femmesh/gmshtools.py | 54 +++--- src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py | 173 +++----------------- 3 files changed, 108 insertions(+), 227 deletions(-) diff --git a/src/Mod/Fem/Gui/Resources/ui/MeshGmsh.ui b/src/Mod/Fem/Gui/Resources/ui/MeshGmsh.ui index a8ae88a12b..be66475ca2 100644 --- a/src/Mod/Fem/Gui/Resources/ui/MeshGmsh.ui +++ b/src/Mod/Fem/Gui/Resources/ui/MeshGmsh.ui @@ -49,38 +49,35 @@ - - - - 0 - 0 - - - - - 80 - 20 - - - - 0 mm - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 1.000000000000000 - - - 1000000000.000000000000000 + + + true mm - - 2 + + + 100 + 20 + - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + 0.000000000000000 + + + 1000000000000000000000.000000000000000 + + + 1.000000000000000 + + 0.000000000000000 @@ -93,38 +90,35 @@ - - - - 0 - 0 - - - - - 80 - 20 - - - - 0 mm - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 1.000000000000000 - - - 1000000000.000000000000000 + + + true mm - - 2 + + + 100 + 20 + - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + 0.000000000000000 + + + 1000000000000000000000.000000000000000 + + + 1.000000000000000 + + 0.000000000000000 @@ -219,9 +213,9 @@ - Gui::InputField - QLineEdit -
Gui/InputField.h
+ Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
diff --git a/src/Mod/Fem/femmesh/gmshtools.py b/src/Mod/Fem/femmesh/gmshtools.py index bbe2bf50f9..0a9bd5419f 100644 --- a/src/Mod/Fem/femmesh/gmshtools.py +++ b/src/Mod/Fem/femmesh/gmshtools.py @@ -46,17 +46,25 @@ class GmshError(Exception): class GmshTools: + + name = "Gmsh" + def __init__(self, gmsh_mesh_obj, analysis=None): # mesh obj self.mesh_obj = gmsh_mesh_obj + self.process = None # analysis if analysis: self.analysis = analysis else: self.analysis = None + self.load_properties() + self.error = False + + def load_properties(self): # part to mesh self.part_obj = self.mesh_obj.Shape @@ -186,7 +194,6 @@ class GmshTools: self.temp_file_geo = "" self.mesh_name = "" self.gmsh_bin = "" - self.error = False def update_mesh_data(self): self.start_logs() @@ -199,6 +206,27 @@ class GmshTools: self.write_part_file() self.write_geo() + def compute(self): + self.load_properties() + self.update_mesh_data() + self.get_tmp_file_paths() + self.get_gmsh_command() + self.write_gmsh_input_files() + + command_list = [self.gmsh_bin, "-", self.temp_file_geo] + self.process = subprocess.Popen( + command_list, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + out, err = self.process.communicate() + if self.process.returncode != 0: + raise RuntimeError(err.decode("utf-8")) + + return True + + def update_properties(self): + self.mesh_obj.FemMesh = Fem.read(self.temp_file_mesh) + def create_mesh(self): try: self.update_mesh_data() @@ -399,7 +427,7 @@ class GmshTools: # if self.group_elements: # Console.PrintMessage(" {}\n".format(self.group_elements)) - def get_gmsh_version(self): + def version(self): self.get_gmsh_command() if os.path.exists(self.gmsh_bin): found_message = "file found: " + self.gmsh_bin @@ -407,7 +435,7 @@ class GmshTools: else: found_message = "file not found: " + self.gmsh_bin Console.PrintError(found_message + "\n") - return (None, None, None), found_message + return found_message command_list = [self.gmsh_bin, "--info"] try: @@ -420,26 +448,10 @@ class GmshTools: ) except Exception as e: Console.PrintMessage(str(e) + "\n") - return (None, None, None), found_message + "\n\n" + "Error: " + str(e) + return found_message + "\n\n" + "Error: " + str(e) gmsh_stdout, gmsh_stderr = p.communicate() - Console.PrintMessage("Gmsh: StdOut:\n" + gmsh_stdout + "\n") - if gmsh_stderr: - Console.PrintError("Gmsh: StdErr:\n" + gmsh_stderr + "\n") - - from re import search - - # use raw string mode to get pep8 quiet - # https://stackoverflow.com/q/61497292 - # https://github.com/MathSci/fecon236/issues/6 - match = search(r"^Version\s*:\s*(\d+)\.(\d+)\.(\d+)", gmsh_stdout) - # return (major, minor, patch), fullmessage - if match: - mess = found_message + "\n\n" + gmsh_stdout - return match.group(1, 2, 3), mess - else: - mess = found_message + "\n\n" + "Warning: Output not recognized\n\n" + gmsh_stdout - return (None, None, None), mess + return gmsh_stdout def get_region_data(self): # mesh regions diff --git a/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py b/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py index 9c35ba266c..32afdba764 100644 --- a/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py +++ b/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py @@ -29,24 +29,17 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief task panel for mesh gmsh object -import sys -import time - from PySide import QtCore -from PySide import QtGui -from PySide.QtCore import Qt -from PySide.QtGui import QApplication import FreeCAD import FreeCADGui -import FemGui -from femtools.femutils import is_of_type -from femtools.femutils import getOutputWinColor -from . import base_femtaskpanel +from femmesh import gmshtools + +from . import base_femmeshtaskpanel -class _TaskPanel(base_femtaskpanel._BaseTaskPanel): +class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): """ The TaskPanel for editing References property of MeshGmsh objects and creation of new FEM mesh @@ -58,16 +51,14 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): self.form = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/MeshGmsh.ui" ) - self.Timer = QtCore.QTimer() - self.Timer.start(100) # 100 milli seconds - self.gmsh_runs = False - self.console_message_gmsh = "" + + self.tool = gmshtools.GmshTools(obj) QtCore.QObject.connect( - self.form.if_max, QtCore.SIGNAL("valueChanged(Base::Quantity)"), self.max_changed + self.form.qsb_max_size, QtCore.SIGNAL("valueChanged(Base::Quantity)"), self.max_changed ) QtCore.QObject.connect( - self.form.if_min, QtCore.SIGNAL("valueChanged(Base::Quantity)"), self.min_changed + self.form.qsb_min_size, QtCore.SIGNAL("valueChanged(Base::Quantity)"), self.min_changed ) QtCore.QObject.connect( self.form.cb_dimension, QtCore.SIGNAL("activated(int)"), self.choose_dimension @@ -75,40 +66,16 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): QtCore.QObject.connect( self.form.cb_order, QtCore.SIGNAL("activated(int)"), self.choose_order ) - QtCore.QObject.connect(self.Timer, QtCore.SIGNAL("timeout()"), self.update_timer_text) - QtCore.QObject.connect( - self.form.pb_get_gmsh_version, QtCore.SIGNAL("clicked()"), self.get_gmsh_version - ) - self.form.cb_dimension.addItems(self.obj.getEnumerationsOfProperty("ElementDimension")) self.form.cb_order.addItems(self.obj.getEnumerationsOfProperty("ElementOrder")) + QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.update_timer_text) + QtCore.QObject.connect( + self.form.pb_get_gmsh_version, QtCore.SIGNAL("clicked()"), self.get_version + ) self.get_mesh_params() - self.get_active_analysis() - self.update() - - def getStandardButtons(self): - button_value = ( - QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Cancel - ) - return button_value - # show a OK, a apply and a Cancel button - # def reject() is called on Cancel button - # def clicked(self, button) is needed, to access the apply button - - def accept(self): - self.set_mesh_params() - return super().accept() - - def reject(self): - self.Timer.stop() - return super().reject() - - def clicked(self, button): - if button == QtGui.QDialogButtonBox.Apply: - self.set_mesh_params() - self.run_gmsh() + self.set_widgets() def get_mesh_params(self): self.clmax = self.obj.CharacteristicLengthMax @@ -122,42 +89,21 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): self.obj.ElementDimension = self.dimension self.obj.ElementOrder = self.order - def update(self): + def set_widgets(self): "fills the widgets" - self.form.if_max.setText(self.clmax.UserString) - self.form.if_min.setText(self.clmin.UserString) + self.form.qsb_max_size.setProperty("value", self.clmax) + FreeCADGui.ExpressionBinding(self.form.qsb_max_size).bind( + self.obj, "CharacteristicLengthMax" + ) + self.form.qsb_min_size.setProperty("value", self.clmin) + FreeCADGui.ExpressionBinding(self.form.qsb_min_size).bind( + self.obj, "CharacteristicLengthMin" + ) index_dimension = self.form.cb_dimension.findText(self.dimension) self.form.cb_dimension.setCurrentIndex(index_dimension) index_order = self.form.cb_order.findText(self.order) self.form.cb_order.setCurrentIndex(index_order) - def console_log(self, message="", outputwin_color_type=None): - self.console_message_gmsh = self.console_message_gmsh + ( - '{:4.1f}: '.format( - getOutputWinColor("Logging"), time.time() - self.Start - ) - ) - if outputwin_color_type: - if outputwin_color_type == "#00AA00": # Success is not part of output window parameters - self.console_message_gmsh += '{}
'.format( - outputwin_color_type, message - ) - else: - self.console_message_gmsh += '{}
'.format( - getOutputWinColor(outputwin_color_type), message - ) - else: - self.console_message_gmsh += message + "
" - self.form.te_output.setText(self.console_message_gmsh) - self.form.te_output.moveCursor(QtGui.QTextCursor.End) - - def update_timer_text(self): - # FreeCAD.Console.PrintMessage("timer1\n") - if self.gmsh_runs: - FreeCAD.Console.PrintMessage("timer2\n") - # FreeCAD.Console.PrintMessage("Time: {0:4.1f}: \n".format(time.time() - self.Start)) - self.form.l_time.setText(f"Time: {time.time() - self.Start:4.1f}: ") - def max_changed(self, base_quantity_value): self.clmax = base_quantity_value @@ -168,81 +114,10 @@ class _TaskPanel(base_femtaskpanel._BaseTaskPanel): if index < 0: return self.form.cb_dimension.setCurrentIndex(index) - self.dimension = str(self.form.cb_dimension.itemText(index)) # form returns unicode + self.dimension = self.form.cb_dimension.itemText(index) def choose_order(self, index): if index < 0: return self.form.cb_order.setCurrentIndex(index) - self.order = str(self.form.cb_order.itemText(index)) # form returns unicode - - def get_gmsh_version(self): - from femmesh import gmshtools - - version, full_message = gmshtools.GmshTools(self.obj, self.analysis).get_gmsh_version() - if version[0] and version[1] and version[2]: - messagebox = QtGui.QMessageBox.information - else: - messagebox = QtGui.QMessageBox.warning - messagebox(None, "Gmsh - Information", full_message) - - def run_gmsh(self): - from femmesh import gmshtools - - gmsh_mesh = gmshtools.GmshTools(self.obj, self.analysis) - QApplication.setOverrideCursor(Qt.WaitCursor) - part = self.obj.Shape - if ( - self.obj.MeshRegionList - and part.Shape.ShapeType == "Compound" - and ( - is_of_type(part, "FeatureBooleanFragments") - or is_of_type(part, "FeatureSlice") - or is_of_type(part, "FeatureXOR") - ) - ): - gmsh_mesh.outputCompoundWarning() - self.Start = time.time() - self.form.l_time.setText(f"Time: {time.time() - self.Start:4.1f}: ") - self.console_message_gmsh = "" - self.gmsh_runs = True - self.console_log("We are going to start ...") - self.get_active_analysis() - self.console_log("Start Gmsh ...") - error = "" - try: - error = gmsh_mesh.create_mesh() - except Exception: - error = sys.exc_info()[1] - FreeCAD.Console.PrintError(f"Unexpected error when creating mesh: {error}\n") - if error: - FreeCAD.Console.PrintWarning("Gmsh had warnings:\n") - FreeCAD.Console.PrintWarning(f"{error}\n") - self.console_log("Gmsh had warnings ...", "Warning") - self.console_log(error, "Error") - else: - FreeCAD.Console.PrintMessage("Clean run of Gmsh\n") - self.console_log("Clean run of Gmsh", "#00AA00") - self.console_log("Gmsh done!") - self.form.l_time.setText(f"Time: {time.time() - self.Start:4.1f}: ") - self.Timer.stop() - self.update() - QApplication.restoreOverrideCursor() - - def get_active_analysis(self): - analysis = FemGui.getActiveAnalysis() - if not analysis: - FreeCAD.Console.PrintLog("No active analysis, means no group meshing.\n") - self.analysis = None # no group meshing - else: - for m in analysis.Group: - if m.Name == self.obj.Name: - FreeCAD.Console.PrintLog(f"Active analysis found: {analysis.Name}\n") - self.analysis = analysis # group meshing - break - else: - FreeCAD.Console.PrintLog( - "Mesh is not member of active analysis, means no group meshing.\n" - ) - self.analysis = None # no group meshing - return + self.order = self.form.cb_order.itemText(index)