Fem: Print real-time log messages in mesher task panels - fixes #17594
This commit is contained in:
committed by
Chris Hennes
parent
38ea260fef
commit
616b78865f
@@ -30,6 +30,7 @@ __url__ = "https://www.freecad.org"
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from PySide.QtCore import QProcess
|
||||
|
||||
import FreeCAD
|
||||
from FreeCAD import Console
|
||||
@@ -54,7 +55,7 @@ class GmshTools:
|
||||
# mesh obj
|
||||
self.mesh_obj = gmsh_mesh_obj
|
||||
|
||||
self.process = None
|
||||
self.process = QProcess()
|
||||
# analysis
|
||||
self.analysis = None
|
||||
if analysis:
|
||||
@@ -210,27 +211,17 @@ class GmshTools:
|
||||
self.write_part_file()
|
||||
self.write_geo()
|
||||
|
||||
def compute(self):
|
||||
def prepare(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,
|
||||
startupinfo=femutils.startProgramInfo("hide"),
|
||||
)
|
||||
|
||||
out, err = self.process.communicate()
|
||||
if self.process.returncode != 0:
|
||||
raise RuntimeError(err.decode("utf-8"))
|
||||
|
||||
return True
|
||||
def compute(self):
|
||||
command_list = ["-v", "4", "-", self.temp_file_geo]
|
||||
self.process.start(self.gmsh_bin, command_list)
|
||||
return self.process
|
||||
|
||||
def update_properties(self):
|
||||
self.mesh_obj.FemMesh = Fem.read(self.temp_file_mesh)
|
||||
|
||||
@@ -27,9 +27,9 @@ __url__ = "https://www.freecad.org"
|
||||
|
||||
import numpy as np
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from PySide.QtCore import QProcess
|
||||
|
||||
import FreeCAD
|
||||
import Fem
|
||||
@@ -81,6 +81,8 @@ class NetgenTools:
|
||||
self.fem_mesh = None
|
||||
self.process = None
|
||||
self.tmpdir = ""
|
||||
self.process = QProcess()
|
||||
self.mesh_params = {}
|
||||
|
||||
def write_geom(self):
|
||||
if not self.tmpdir:
|
||||
@@ -101,9 +103,9 @@ from femmesh.netgentools import NetgenTools
|
||||
NetgenTools.run_netgen(**{params})
|
||||
"""
|
||||
|
||||
def compute(self):
|
||||
def prepare(self):
|
||||
self.write_geom()
|
||||
mesh_params = {
|
||||
self.mesh_params = {
|
||||
"brep_file": self.brep_file,
|
||||
"threads": self.obj.Threads,
|
||||
"heal": self.obj.HealShape,
|
||||
@@ -112,19 +114,11 @@ NetgenTools.run_netgen(**{params})
|
||||
"result_file": self.result_file,
|
||||
}
|
||||
|
||||
code_str = self.code.format(params=mesh_params)
|
||||
def compute(self):
|
||||
code_str = self.code.format(params=self.mesh_params)
|
||||
self.process.start(sys.executable, ["-c", code_str])
|
||||
|
||||
cmd_list = [
|
||||
sys.executable,
|
||||
"-c",
|
||||
code_str,
|
||||
]
|
||||
self.process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = self.process.communicate()
|
||||
if self.process.returncode != 0:
|
||||
raise RuntimeError(err.decode("utf-8"))
|
||||
|
||||
return True
|
||||
return self.process
|
||||
|
||||
@staticmethod
|
||||
def run_netgen(brep_file, threads, heal, params, second_order, result_file):
|
||||
|
||||
@@ -30,7 +30,6 @@ __url__ = "https://www.freecad.org"
|
||||
# \brief base task panel for mesh object
|
||||
|
||||
import time
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from PySide import QtCore
|
||||
@@ -43,48 +42,19 @@ from femtools.femutils import getOutputWinColor
|
||||
from . import base_femtaskpanel
|
||||
|
||||
|
||||
class _Process(threading.Thread):
|
||||
class _Thread(QtCore.QThread):
|
||||
"""
|
||||
Class for thread and subprocess manipulation
|
||||
'tool' argument must be an object with a 'compute' method
|
||||
and a 'process' attribute of type Popen object
|
||||
'tool' argument must be an object with 'compute' and 'prepare' methods
|
||||
and a 'process' attribute of type QProcess object
|
||||
"""
|
||||
|
||||
def __init__(self, tool):
|
||||
super().__init__()
|
||||
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
|
||||
self.tool.prepare()
|
||||
|
||||
|
||||
class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC):
|
||||
@@ -92,14 +62,79 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC):
|
||||
Abstract base class for FemMesh object TaskPanel
|
||||
"""
|
||||
|
||||
def __init__(self, obj):
|
||||
def __init__(self, obj, tool):
|
||||
super().__init__(obj)
|
||||
|
||||
self.tool = None
|
||||
self.form = None
|
||||
self.tool = tool
|
||||
self.timer = QtCore.QTimer()
|
||||
self.process = None
|
||||
self.console_message = ""
|
||||
self._thread = _Thread(self.tool)
|
||||
self.text_log = None
|
||||
self.text_time = None
|
||||
|
||||
def setup_connections(self):
|
||||
QtCore.QObject.connect(self._thread, QtCore.SIGNAL("started()"), self.thread_started)
|
||||
QtCore.QObject.connect(self._thread, QtCore.SIGNAL("finished()"), self.thread_finished)
|
||||
QtCore.QObject.connect(self.tool.process, QtCore.SIGNAL("started()"), self.process_started)
|
||||
QtCore.QObject.connect(
|
||||
self.tool.process,
|
||||
QtCore.SIGNAL("finished(int,QProcess::ExitStatus)"),
|
||||
self.process_finished,
|
||||
)
|
||||
QtCore.QObject.connect(
|
||||
self.tool.process,
|
||||
QtCore.SIGNAL("readyReadStandardOutput()"),
|
||||
self.write_output,
|
||||
)
|
||||
QtCore.QObject.connect(
|
||||
self.tool.process,
|
||||
QtCore.SIGNAL("readyReadStandardError()"),
|
||||
self.write_error,
|
||||
)
|
||||
QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.update_timer_text)
|
||||
|
||||
def thread_started(self):
|
||||
self.text_log.clear()
|
||||
self.write_log("Prepare meshing...\n", QtGui.QColor(getOutputWinColor("Text")))
|
||||
QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
|
||||
|
||||
def thread_finished(self):
|
||||
self.tool.compute()
|
||||
|
||||
def process_finished(self, code, status):
|
||||
if status == QtCore.QProcess.ExitStatus.NormalExit:
|
||||
if code != 0:
|
||||
self.write_log(
|
||||
"Meshing finished with errors\n", QtGui.QColor(getOutputWinColor("Error"))
|
||||
)
|
||||
self.tool.update_properties()
|
||||
self.write_log("Process finished\n", QtGui.QColor(getOutputWinColor("Text")))
|
||||
else:
|
||||
self.write_log("Process crashed\n", QtGui.QColor(getOutputWinColor("Error")))
|
||||
|
||||
def process_started(self):
|
||||
self.write_log("Start meshing...\n", QtGui.QColor(getOutputWinColor("Text")))
|
||||
|
||||
def write_output(self):
|
||||
self.write_log(
|
||||
self.tool.process.readAllStandardOutput().data().decode("utf-8"),
|
||||
QtGui.QColor(getOutputWinColor("Logging")),
|
||||
)
|
||||
|
||||
def write_error(self):
|
||||
self.write_log(
|
||||
self.tool.process.readAllStandardError().data().decode("utf-8"),
|
||||
QtGui.QColor(getOutputWinColor("Error")),
|
||||
)
|
||||
|
||||
def write_log(self, data, color):
|
||||
cursor = QtGui.QTextCursor(self.text_log.document())
|
||||
cursor.beginEditBlock()
|
||||
cursor.movePosition(QtGui.QTextCursor.End)
|
||||
fmt = QtGui.QTextCharFormat()
|
||||
fmt.setForeground(color)
|
||||
cursor.mergeCharFormat(fmt)
|
||||
cursor.insertText(data)
|
||||
cursor.endEditBlock()
|
||||
self.text_log.ensureCursorVisible()
|
||||
|
||||
@abstractmethod
|
||||
def set_mesh_params(self):
|
||||
@@ -116,7 +151,10 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC):
|
||||
return button_value
|
||||
|
||||
def accept(self):
|
||||
if self.process and self.process.is_alive():
|
||||
if (
|
||||
self._thread.isRunning()
|
||||
or self.tool.process.state() != QtCore.QProcess.ProcessState.NotRunning
|
||||
):
|
||||
FreeCAD.Console.PrintWarning("Process still running\n")
|
||||
return None
|
||||
|
||||
@@ -126,62 +164,46 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC):
|
||||
return super().accept()
|
||||
|
||||
def reject(self):
|
||||
# self_thread may be blocking
|
||||
if self._thread.isRunning():
|
||||
return None
|
||||
self.timer.stop()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
if self.process and self.process.is_alive():
|
||||
self.console_log("Process aborted", "#ff6700")
|
||||
self.process.finish()
|
||||
if self.tool.process.state() != QtCore.QProcess.ProcessState.NotRunning:
|
||||
self.tool.process.kill()
|
||||
self._thread.quit()
|
||||
FreeCAD.Console.PrintWarning("Process aborted\n")
|
||||
else:
|
||||
return super().reject()
|
||||
|
||||
def clicked(self, button):
|
||||
if button == QtGui.QDialogButtonBox.Apply:
|
||||
if self.process and self.process.is_alive():
|
||||
if (
|
||||
self._thread.isRunning()
|
||||
or self.tool.process.state() != QtCore.QProcess.ProcessState.NotRunning
|
||||
):
|
||||
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 + (
|
||||
'<font color="{}"><b>{:4.1f}:</b></font> '.format(
|
||||
getOutputWinColor("Logging"), time.time() - self.time_start
|
||||
)
|
||||
)
|
||||
if outputwin_color_type:
|
||||
self.console_message += '<font color="{}">{}</font><br>'.format(
|
||||
outputwin_color_type, message
|
||||
)
|
||||
else:
|
||||
self.console_message += message + "<br>"
|
||||
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}: ")
|
||||
if (
|
||||
self._thread.isRunning()
|
||||
or self.tool.process.state() != QtCore.QProcess.ProcessState.NotRunning
|
||||
):
|
||||
self.text_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.text_time.setText(f"Time: {time.time() - self.time_start:4.1f}: ")
|
||||
|
||||
self.process.init()
|
||||
self._thread.start()
|
||||
|
||||
def get_version(self):
|
||||
full_message = self.tool.version()
|
||||
|
||||
@@ -46,13 +46,18 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel):
|
||||
"""
|
||||
|
||||
def __init__(self, obj):
|
||||
super().__init__(obj)
|
||||
super().__init__(obj, gmshtools.GmshTools(obj))
|
||||
|
||||
self.form = FreeCADGui.PySideUic.loadUi(
|
||||
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/MeshGmsh.ui"
|
||||
)
|
||||
self.text_log = self.form.te_output
|
||||
self.text_time = self.form.l_time
|
||||
|
||||
self.tool = gmshtools.GmshTools(obj)
|
||||
self.setup_connections()
|
||||
|
||||
def setup_connections(self):
|
||||
super().setup_connections()
|
||||
|
||||
QtCore.QObject.connect(
|
||||
self.form.qsb_max_size, QtCore.SIGNAL("valueChanged(Base::Quantity)"), self.max_changed
|
||||
@@ -69,7 +74,6 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel):
|
||||
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
|
||||
)
|
||||
|
||||
@@ -46,12 +46,19 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel):
|
||||
"""
|
||||
|
||||
def __init__(self, obj):
|
||||
super().__init__(obj)
|
||||
super().__init__(obj, netgentools.NetgenTools(obj))
|
||||
|
||||
self.form = FreeCADGui.PySideUic.loadUi(
|
||||
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/MeshNetgen.ui"
|
||||
)
|
||||
|
||||
self.tool = netgentools.NetgenTools(obj)
|
||||
self.text_log = self.form.te_output
|
||||
self.text_time = self.form.l_time
|
||||
|
||||
self.setup_connections()
|
||||
|
||||
def setup_connections(self):
|
||||
super().setup_connections()
|
||||
|
||||
QtCore.QObject.connect(
|
||||
self.form.qsb_max_size,
|
||||
@@ -86,7 +93,6 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel):
|
||||
QtCore.SIGNAL("currentIndexChanged(int)"),
|
||||
self.fineness_changed,
|
||||
)
|
||||
QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.update_timer_text)
|
||||
QtCore.QObject.connect(
|
||||
self.form.pb_get_netgen_version, QtCore.SIGNAL("clicked()"), self.get_version
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user