From: Konstantin Leontev Date: Tue, 9 Jan 2024 12:25:23 +0000 (+0000) Subject: [bos #38044][EDF] (2023-T3) Support for automatic reparation. Added execution chain... X-Git-Url: http://git.salome-platform.org/gitweb/?a=commitdiff_plain;h=5995197696e95073d2afbe921dfd2f8fd359c173;p=modules%2Fgeom.git [bos #38044][EDF] (2023-T3) Support for automatic reparation. Added execution chain and generic GUI using merge faces plugin as an example. --- diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4399dd140..5f9654ef9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,6 +25,7 @@ SET(SUBDIRS_COMMON GEOMImpl GEOM_I GEOMClient GEOM_I_Superv GEOM_SWIG GEOM_PY AdvancedEngine STLPlugin BREPPlugin STEPPlugin IGESPlugin XAOPlugin Tools + RepairGUIAdv ) ## diff --git a/src/RepairGUIAdv/CMakeLists.txt b/src/RepairGUIAdv/CMakeLists.txt new file mode 100644 index 000000000..d2251be45 --- /dev/null +++ b/src/RepairGUIAdv/CMakeLists.txt @@ -0,0 +1,70 @@ +# Copyright (C) 2012-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +IF(SALOME_BUILD_GUI) + INCLUDE(UsePyQt) + + # We're already using these ui templates for C++ RepairGUI + SET(geom_dlg_ref ../DlgRef) + + # scripts / static + SET(plugin_SCRIPTS + geomrepairadv_plugins.py + ) + + # base scripts + SET(_base_SCRIPTS + geomrepairadv_common.py + geomrepairadv_execute.py + geomrepairadv_logger.py + geomrepairadv_progress.py + geomrepairadv_worker.py + locate_subshapes.py + merge_faces.py + merge_faces_algo.py + union_edges.py + ) + + # gui scripts + SET(_gui_SCRIPTS + basedlg.py + basedlg.ui + ${geom_dlg_ref}/DlgRef_1Sel_QTD.ui + DlgRef_1Spin_QTD.ui # copied because original was promoted to SalomeApp_DoubleSpinBox + ) + + # uic files / to be processed by pyuic + SET(_pyuic_FILES + basedlg.ui + ${geom_dlg_ref}/DlgRef_1Sel_QTD.ui + DlgRef_1Spin_QTD.ui # copied because original was promoted to SalomeApp_DoubleSpinBox + ) + + # scripts / pyuic wrappings + PYQT_WRAP_UIC(_pyuic_SCRIPTS ${_pyuic_FILES} TARGET_NAME _target_name_pyuic) + + # --- rules --- + SALOME_INSTALL_SCRIPTS("${plugin_SCRIPTS}" ${SALOME_GEOM_INSTALL_PLUGINS}) + SALOME_INSTALL_SCRIPTS("${_base_SCRIPTS}" ${SALOME_INSTALL_PYTHON}/salome/geom/geomrepairadv) + SALOME_INSTALL_SCRIPTS("${_gui_SCRIPTS}" ${SALOME_INSTALL_PYTHON}/salome/geom/geomrepairadv) + SALOME_INSTALL_SCRIPTS("${_pyuic_SCRIPTS}" ${SALOME_INSTALL_PYTHON}/salome/geom/geomrepairadv TARGET_NAME _target_name_pyuic_py) + # add dependency of compiled py files on uic files in order + # to avoid races problems when compiling in parallel + ADD_DEPENDENCIES(${_target_name_pyuic_py} ${_target_name_pyuic}) +ENDIF() diff --git a/src/RepairGUIAdv/DlgRef_1Spin_QTD.ui b/src/RepairGUIAdv/DlgRef_1Spin_QTD.ui new file mode 100644 index 000000000..da509087e --- /dev/null +++ b/src/RepairGUIAdv/DlgRef_1Spin_QTD.ui @@ -0,0 +1,81 @@ + + + DlgRef_1Spin_QTD + + + + 0 + 0 + 114 + 51 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 9 + + + 9 + + + 9 + + + 9 + + + 6 + + + + + + 0 + 0 + + + + TL1 + + + false + + + + + + + + + + + + + qPixmapFromMimeSource + + + diff --git a/src/RepairGUIAdv/basedlg.py b/src/RepairGUIAdv/basedlg.py new file mode 100644 index 000000000..26524ca36 --- /dev/null +++ b/src/RepairGUIAdv/basedlg.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import sys +from pathlib import Path +from traceback import format_exc + +from qtsalome import Qt, QWidget, QMessageBox, QApplication, QGridLayout + +from salome.gui import helper +from salome.kernel.studyedit import EDITOR +from salome.kernel.services import IDToObject, ObjectToID +from salome.geom import geomBuilder +from salome.geom.geomtools import GeomStudyTools +from libGEOM_Swig import GEOM_Swig + +from .basedlg_ui import Ui_BaseDlg +from .geomrepairadv_execute import execute +from .geomrepairadv_logger import logger +from .geomrepairadv_common import DlgRef_1Sel_QTD, \ + GEOM_RESULT_NAME_GRP, NAME_LBL, GEOM_SELECTED_LBL, GEOM_SELECTED_SHAPE +import GEOM + +class BaseDlg(Ui_BaseDlg, QWidget): + """ + Base dialog for all GEOM repair widgets. + Manages standard buttons (Apply and Close, Apply, Close, Help) and + adds a child widget specific for each algorithm that uses + this dialog as a base class. + """ + + # Collection of derived singletons + _instances = {} + + def __new__(cls, *args, **kwargs): + """ + Returns a singleton instance of the plugin's dialog. + It is mandatory in order to call show without a parent. + """ + if cls._instances.get(cls, None) is None: + cls._instances[cls] = super(BaseDlg, cls).__new__(cls, *args, **kwargs) + + return BaseDlg._instances[cls] + + def __init__(self, child_widget, window_title, algo_name, is_default_location, selection_level): + """ + First inits the base part of dialog, + then puts in place a widget, implemented for a child class. + + Args: + child_widget - object to display algorithm specific UI + window_title - string to display in the dialog's title bar + algo_name - path to specific algorithm module + is_default_location - if True, then algo file in the same directory. + """ + QWidget.__init__(self) + + # Set up the generic user interface from Designer. + self.setupUi(self) + + self.setWindowTitle(window_title) + + # Selection widgets are common for every algorithm at the moment + + # Widget for result shape + # Prepend a result name with a window title without spaces + self._result_name = ''.join(window_title.split()) + '_' + self._result_widget = DlgRef_1Sel_QTD() + self._result_widget.GroupBox1.setTitle(GEOM_RESULT_NAME_GRP) + self._result_widget.TextLabel1.setText(NAME_LBL) + self._result_widget.LineEdit1.setText(self._result_name) + self._result_widget.PushButton1.hide() + + # Widget for selected shape + self._selected_widget = DlgRef_1Sel_QTD() + self._selected_widget.GroupBox1.setTitle(GEOM_SELECTED_LBL) + self._selected_widget.TextLabel1.setText(GEOM_SELECTED_SHAPE) + self._selected_widget.PushButton1.clicked.connect(self.on_select_object) + + # Keep references to selected object and its temporary copy + # that we need to pass for execution instead of original one. + # TODO: decide if we really need to pass a copy. + self._selected_object = None + self._selected_copy = None + + # Put the common widgets and a child widget for a specific algorithm + # in a place right above standard buttons (defined by child_placeholder). + self.child_layout = QGridLayout(self.child_placeholder) + self.child_layout.setContentsMargins(0, 0, 0, 0) + self.child_layout.addWidget(self._result_widget, 0, 0) + self.child_layout.addWidget(self._selected_widget, 1, 0) + if child_widget: + self.child_layout.addWidget(child_widget, 2, 0) + + # Set basic button's actions + self.buttonOk.clicked.connect(self.on_apply_close) + self.buttonApply.clicked.connect(self.on_apply) + self.buttonClose.clicked.connect(self.close) + self.buttonHelp.clicked.connect(self.on_help) + + # Execution module + # Name of particular algo module for each repair class + self._algo_name = '' + self.set_algoname(algo_name, is_default_location) + + # Let it be always on top of the application. + # We need it because this dialog will run without parent. + self.setWindowFlags(Qt.WindowStaysOnTopHint) + + # Default selection level + self._selection_level = selection_level + + # Check if we already have selected object + self.on_select_object() + + + def on_apply_close(self): + """ + Calls on pressing Apply and Close button. + """ + self.execute() + self.close() + + + def on_apply(self): + """ + Calls on pressing Apply button. + """ + self.execute() + + + def on_help(self): + """ + Calls on pressing Help button. + """ + QMessageBox.about(None, "Help", "Not implemented yet") + + + def get_args(self): + """ + Collects arguments for a repair execution algorithm into a dictionary. + + Args: + None. + + Returns: + Dictionary with arguments for execution. + """ + + return {} + + + def execute(self): + """ + Executes actual algorithm. + + Args: + None. + + Returns: + None + """ + + if not self._selected_object: + QMessageBox.warning( + None, + 'Warning', + 'You must select an object to repair!' + ) + return + + # Make copy to prevent unintentional changing of a source object from the algo script + builder = geomBuilder.New() + self._selected_copy = builder.MakeCopy( + self._selected_object, self.get_result_name() + '_temp') + + args_dict = self.get_args() + if args_dict: + # Add the copy object first + args_dict['source_solid'] = self._selected_copy + + execute(self._algo_name, args_dict) + # TODO: do we need to handle here a case if the algo failed? + + # Delete a copy object in any case + copy_entry = ObjectToID(self._selected_copy) + tools = GeomStudyTools() + tools.deleteShape(copy_entry) + self._selected_copy = None + + + def set_algoname(self, algo_name, is_default_location): + """ + Sets the path to the algorithm. + + Args: + algo_name - an algorithm's name. + is_default_location - if True, then algo file in the same directory. + + Returns: + None + """ + + if is_default_location: + package_dir = Path(__file__).parent.absolute() + self._algo_name = package_dir.joinpath(algo_name) + else: + self._algo_name = algo_name + + + def set_result_name(self, name): + """ + Sets a name of the result shape. + + Args: + name - a provided name. + + Returns: + None. + """ + + self._result_widget.LineEdit1.setText(name) + + + def get_result_name(self): + """ + Sets a name of the result shape. + + Args: + None. + + Returns: + A name in the related edit line of the dialog. + """ + + return self._result_widget.LineEdit1.text() + + + def set_selection(self, entry = None): + """ + Sets selection level to self._selection_level or resets it. + + Args: + entry - an item currently selected in the objects browser. + + Returns: + None. + """ + + if not self._selection_level: + return + + geom_swig = GEOM_Swig() + + # Resets selection level + geom_swig.closeLocalSelection() + + # Set level of selection for specific entry + if entry: + sel_level = geomBuilder.EnumToLong(self._selection_level) + geom_swig.initLocalSelection(entry, sel_level) + + + def on_select_object(self): + """ + Adds selected object to a dialog. + + Args: + None. + + Returns: + None. + """ + + # Get selected object + sobject, entry = helper.getSObjectSelected() + + # Update selected widget and object + if sobject and entry: + source_name = EDITOR.getName(sobject) + self.set_result_name(self._result_name + source_name) + self._selected_widget.LineEdit1.setText(source_name) + self._selected_object = IDToObject(entry, EDITOR.study) + else: + self.set_result_name(self._result_name) + self._selected_widget.LineEdit1.clear() + self._selected_object = None + entry = None + + # Selection level + self.set_selection(entry) + + + def closeEvent(self, event): + """ + Overrides default close envent to reset selection level. + """ + + super().closeEvent(event) + self.set_selection(None) + + +# For testing run as a module from geomrepairadv parent directory in +# Salome INSTALL, because the dialog needs a generated Ui_BaseDlg class +# that we don't have in the SOURCE. +# Example: +# $ python -m geomrepairadv.basedlg +if __name__ == '__main__': + app = QApplication(sys.argv) + + dlg = BaseDlg(None, 'Test base dialog', 'test_algo', True, None) + dlg.show() + + sys.exit(app.exec_()) diff --git a/src/RepairGUIAdv/basedlg.ui b/src/RepairGUIAdv/basedlg.ui new file mode 100644 index 000000000..6f82f207b --- /dev/null +++ b/src/RepairGUIAdv/basedlg.ui @@ -0,0 +1,111 @@ + + + BaseDlg + + + + 0 + 0 + 411 + 278 + + + + + + + + + + + 0 + 0 + + + + + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + &Apply and Close + + + + + + + &Apply + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 91 + 0 + + + + + + + + &Close + + + + + + + &Help + + + + + + + + + + + 0 + 0 + + + + + + + + buttonOk + buttonApply + buttonClose + buttonHelp + + + + diff --git a/src/RepairGUIAdv/geomrepairadv_common.py b/src/RepairGUIAdv/geomrepairadv_common.py new file mode 100644 index 000000000..06e91a039 --- /dev/null +++ b/src/RepairGUIAdv/geomrepairadv_common.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +from qtsalome import QWidget +from SalomePyQt import SalomePyQt + +from salome.geom.geomrepairadv.DlgRef_1Sel_QTD_ui import Ui_DlgRef_1Sel_QTD +from salome.geom.geomrepairadv.DlgRef_1Spin_QTD_ui import Ui_DlgRef_1Spin_QTD + +# Constants from /src/GEOMGUI/GEOM_msg_en.ts +GEOM_RESULT_NAME_GRP = 'Result name' +NAME_LBL = 'Name' +GEOM_SELECTED_LBL = 'Name' +GEOM_SELECTED_SHAPE = 'Selected shape' + +class DlgRef_1Sel_QTD(Ui_DlgRef_1Sel_QTD, QWidget): + """ + Helper class to set up a widget for any related dialog. + We need it because a class generated from ui file is derived from an object and + cannot be added as a widget to a dialog's layout. + """ + def __init__(self): + QWidget.__init__(self) + # Set up the user interface from Designer. + self.setupUi(self) + + # Set 'select' icon + icon = SalomePyQt.loadIcon('GEOM', 'select1.png') + self.PushButton1.setIcon(icon) + + +class DlgRef_1Spin_QTD(Ui_DlgRef_1Spin_QTD, QWidget): + """ + Helper class to set up a widget for any related dialog. + We need it because a class generated from ui file is derived from an object and + cannot be added as a widget to a dialog's layout. + """ + def __init__(self): + QWidget.__init__(self) + # Set up the user interface from Designer. + self.setupUi(self) diff --git a/src/RepairGUIAdv/geomrepairadv_execute.py b/src/RepairGUIAdv/geomrepairadv_execute.py new file mode 100644 index 000000000..04e488a3a --- /dev/null +++ b/src/RepairGUIAdv/geomrepairadv_execute.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import os +import sys +import importlib.util + +from .geomrepairadv_progress import RepairProgressDialog +from .geomrepairadv_logger import logger + +from qtsalome import Qt, QApplication, QFileDialog + + +# Testing +import salome +from salome.geom import geomBuilder + + +def module_from_filename(filename): + """ + Create and execute a module by filename. + + Args: + filename - a given python filename. + + Returns: + Module. + """ + + # Get the module from the filename + basename = os.path.basename(filename) + module_name, _ = os.path.splitext(basename) + + spec = importlib.util.spec_from_file_location(module_name, filename) + if not spec: + logger.error('Could not get a spec for %s file!', filename) + return None + + module = importlib.util.module_from_spec(spec) + if not module: + logger.error('Could not get a module for %s file!', filename) + return None + + sys.modules[module_name] = module + + if not spec.loader: + logger.error('spec.loader is None for %s module!', module_name) + return None + + spec.loader.exec_module(module) + + return module + + +def execute(algo_name, args_dict): + """ + Executes GEOM advanced repair algorithm. + + Args: + algo_name - path to the algo module + args_dict - dictionary with arguments those are specific for each algo. + + Returns: + False if the algo failed. + """ + + logger.debug('execute() start') + + # Find a module to execute + algo_module = module_from_filename(algo_name) + logger.debug('algo_module: %s', algo_module) + if not algo_module: + return False + + logger.debug('Create RepairProgressDialog...') + progress_dlg = RepairProgressDialog(parent=None, target=algo_module.run, args=args_dict) + result = progress_dlg.exec() + logger.info('result: %s', result) + + +def test_execution(): + """ + Tests execution of repair algo script. + It uses PartitionCube.brep file to run merge_faces algorithm. + + Because of relative import must be run from a parent dir as a module: + $ python -m RepairGUIAdv.geomrepairadv_execute + """ + + salome.salome_init() + geompy = geomBuilder.New() + + cube_file, _ = QFileDialog.getOpenFileName(None, 'Open brep', '/home', 'Brep Files (*.brep)') + if not cube_file: + return + + # cube_file = "PartitionCube.brep" + source_solid = geompy.ImportBREP(cube_file) + + # Récupération des faces à fusionner + face_a = geompy.GetFaceNearPoint(source_solid, geompy.MakeVertex(-143, -127, 250)) + face_b = geompy.GetFaceNearPoint(source_solid, geompy.MakeVertex(49,-127,250)) + + geompy.addToStudy(source_solid, "source_solid") + geompy.addToStudyInFather(source_solid, face_a, "face_a") + geompy.addToStudyInFather(source_solid, face_b, "face_b") + + + args_dict = { + 'source_solid': source_solid, + 'face_a': face_a, + 'face_b': face_b, + 'result_name': 'MergeFaces_result' + } + + current_dir = os.path.dirname(os.path.realpath(__file__)) + algo_filename, _ = QFileDialog.getOpenFileName( + None, 'Open alogrithm script', current_dir, 'Python Files (*.py)') + if not algo_filename: + return + + execute(algo_filename, args_dict) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + test_execution() + sys.exit(app.exec_()) diff --git a/src/RepairGUIAdv/geomrepairadv_logger.py b/src/RepairGUIAdv/geomrepairadv_logger.py new file mode 100644 index 000000000..b3f877c2c --- /dev/null +++ b/src/RepairGUIAdv/geomrepairadv_logger.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import logging + +from qtsalome import QPlainTextEdit, pyqtSignal, QObject, pyqtSlot + +from salome.kernel.logger import Logger +logger = Logger("salome.geom.geomrepairadv", level = logging.DEBUG) + + +class QTextEditLogger(QObject, logging.Handler): + """ + Provides a QPlainTextEdit text_widget that automaticly filled + by logs text from the logger handler. + """ + + append_text = pyqtSignal(str) + + def __init__(self, parent): + super().__init__(parent) + super(logging.Handler).__init__() + + + self.text_widget = QPlainTextEdit(parent) + self.text_widget.setReadOnly(True) + self.append_text.connect(self.write_log) + + formatter = logging.Formatter( + '%(levelname)s : %(asctime)s : [%(filename)s:%(funcName)s:%(lineno)s] : %(message)s') + self.setFormatter(formatter) + + def emit(self, record): + msg = self.format(record) + self.append_text.emit(msg) + + + @pyqtSlot(str) + def write_log(self, log_text): + """ + Appends a given log to the text widget. + """ + + self.text_widget.appendPlainText(log_text) + self.text_widget.centerCursor() # scroll to the bottom diff --git a/src/RepairGUIAdv/geomrepairadv_plugins.py b/src/RepairGUIAdv/geomrepairadv_plugins.py new file mode 100644 index 000000000..ddf58bab6 --- /dev/null +++ b/src/RepairGUIAdv/geomrepairadv_plugins.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import salome_pluginsmanager + +# Plugins entry points +# For new plugins create a function that shows related dialog, +# then add it into plugin manager below. + +def locate_subshapes(context): + """ + Opens Locate Subshapes plugin's dialog. + """ + from salome.geom.geomrepairadv.locate_subshapes import LocateSubShapesDlg + dialog = LocateSubShapesDlg() + dialog.show() + +def merge_faces(context): + """ + Opens Merge Faces plugin's dialog. + """ + from salome.geom.geomrepairadv.merge_faces import MergeFacesDlg + dialog = MergeFacesDlg() + dialog.show() + +def union_edges(context): + """ + Opens Union Edges plugin's dialog. + """ + from salome.geom.geomrepairadv.union_edges import UnionEdgesDlg + dialog = UnionEdgesDlg() + dialog.show() + + +# Add plugins to a manager with a given menu titles and tooltips + +salome_pluginsmanager.AddFunction( + 'Locate Subshapes', + 'Locates the sub-shapes of a compound by length, area or volume depending on whether it is an ' + 'EDGE, a FACE or a SOLID', + locate_subshapes) + +salome_pluginsmanager.AddFunction( + 'Merge Faces', + 'Merges selected faces with a given precision', + merge_faces) + +salome_pluginsmanager.AddFunction( + 'Union Edges', + 'Merges edges of selected face', + union_edges) diff --git a/src/RepairGUIAdv/geomrepairadv_progress.py b/src/RepairGUIAdv/geomrepairadv_progress.py new file mode 100644 index 000000000..8acd46803 --- /dev/null +++ b/src/RepairGUIAdv/geomrepairadv_progress.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import sys +import logging +from time import sleep + +from qtsalome import QApplication, QPlainTextEdit, \ + QDialog, QVBoxLayout, QProgressDialog, QPushButton + +from .geomrepairadv_logger import QTextEditLogger +from .geomrepairadv_worker import Worker + + +class RepairProgressDialog(QDialog, QPlainTextEdit): + """ + Dialog to show progress bar and log window during of execution + of a shape repair script. + """ + + def __init__(self, parent=None, target=None, args=None): + """ + Inits progress dialogs widgets and runs a target function in a thread. + + Args: + parent - parent dialog + target - target function to run + args - arguments to pass into a target. + """ + + super().__init__(parent) + + self.setWindowTitle('Shape Repair') + + # QProgressDialog uses manual layout, then it's not easy to customize it. + # So, we use it as a widget that is embedded into the dialog. + self.progress = QProgressDialog() + self.progress.setLabelText('Operation in progress...') + self.progress.setAutoClose(False) + self.progress.setAutoReset(False) + + # Override default cancel slot to prevent progress from hiding + self.cancel_button = self.progress.findChild(QPushButton) + self.cancel_button.clicked.disconnect(self.progress.canceled) + self.cancel_button.clicked.connect(self.cancel) + + # Helper flag to decide if we need to change button or close the dialog + self.canceled = False + + # Set logger to redirect logs output into the text widget + self.log_handler = QTextEditLogger(self) + logging.getLogger().addHandler(self.log_handler) + logging.getLogger().setLevel(logging.DEBUG) + + # Layout widgets + layout = QVBoxLayout(self) + + layout.addWidget(self.log_handler.text_widget) + layout.addWidget(self.progress) + + self.setLayout(layout) + + # Setup and run target function in a working thread + self.thread = Worker(parent=self, target=target, args=args) + self.thread.start() + + + def cancel(self): + """ + Replicates QProgressDialog.cancel() method. + """ + + # We need to keep the dialog opened on the first click + # so we can see the log output. + # After that we can close it with a second click. + if self.canceled: + self.close() + else: + # Terminates the execution of the thread. + # TODO: find out if we can do it with requestInterruption() + self.thread.terminate() + + self.progress.setLabelText('Canceled!') + self.progress.setCancelButtonText('Close') + + # Next click we exit + self.canceled = True + + + def on_failed(self): + """ + Decided what to do if opreation failed. + """ + + self.progress.setLabelText('Operation failed!') + self.progress.setCancelButtonText('Close') + + self.canceled = True + + + def on_completed(self): + """ + Decided what to do when opreation completed. + """ + + self.progress.setLabelText('Completed!') + self.progress.setCancelButtonText('Close') + + self.canceled = True + + + def value(self): + """ + Replicates QProgressDialog.value() method. + """ + + return self.progress.value() + + + def setValue(self, progress): + """ + Replicates QProgressDialog.setValue() method. + + Args: + progress - a new value for a progress bar. + """ + + return self.progress.setValue(progress) + + + def close(self): + """ + Process a close event to remove a log handler. + """ + + logging.getLogger().removeHandler(self.log_handler) + + super().close() + + +def test_thread(): + """ + Tests running a test function in a thread while + show a progress with RepairProgressDialog dialog. + + Because of relative import must be run from a parent dir as a module: + $ python -m RepairGUIAdv.geomrepairadv_progress + """ + + progress_dlg = RepairProgressDialog(parent=None, target=test, args=None) + result = progress_dlg.exec() + logging.info('result: %s', result) + + +def test(args, progress_emitter): + """ + Tests logging and progress update with RepairProgressDialog. + """ + + if args: + pass + + progress_emitter.emit() + logging.debug('debug msg') + + sleep(2) + + progress_emitter.emit() + logging.info('info msg') + + sleep(2) + + progress_emitter.emit() + logging.warning('warning msg') + + sleep(2) + + logging.error('error msg') + progress_emitter.emit() + + sleep(2) + + progress_emitter.emit() + + +if __name__ == '__main__': + app = QApplication(sys.argv) + test_thread() + sys.exit(app.exec_()) diff --git a/src/RepairGUIAdv/geomrepairadv_worker.py b/src/RepairGUIAdv/geomrepairadv_worker.py new file mode 100644 index 000000000..2a0acaf62 --- /dev/null +++ b/src/RepairGUIAdv/geomrepairadv_worker.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import logging +import inspect +from traceback import format_exc + +from qtsalome import QApplication, pyqtSignal, QThread, Qt +from .geomrepairadv_logger import logger + + +class Worker(QThread): + """ + Creates a tread to run a given target function with a progress dialog as a parent. + """ + + progress_update = pyqtSignal(int) + thread_failed = pyqtSignal() + work_completed = pyqtSignal() + + def __init__(self, parent=None, target=None, args=None): + super().__init__(parent) + + # Set target function and it's arguments + self.target = target + self.args = args + + # Update a progress bar each time we receive an update signal + self.progress_update.connect(parent.setValue) + self.thread_failed.connect(parent.on_failed) + self.work_completed.connect(parent.on_completed) + + # Calculate total amount of lines in executed function + source_lines = inspect.getsourcelines(target) + total_lines = len(source_lines[0]) + first_line = source_lines[1] + + # Set a progress emitter to update the progress from the target + self.progress_emitter = ProgressEmitter(self.progress_update, total_lines, first_line) + + + def run(self): + """ + Runs the given target function. + """ + + try: + # Wait mode cursor + QApplication.setOverrideCursor(Qt.WaitCursor) + + self.target(self.args, self.progress_emitter) + + # Reset the progress when finished + self.progress_update.emit(100) + self.work_completed.emit() + + except Exception: + logger.error(format_exc()) + self.thread_failed.emit() + + finally: + QApplication.restoreOverrideCursor() + + + def terminate(self): + """ + Overrides default terminate() to add some clean up. + """ + + super().terminate() + + # Termination doesn't call a final block inside run() + QApplication.restoreOverrideCursor() + + +class ProgressEmitter(): + """ + Helper class to reduce code repetition while update progress + from a function executed in a separated thread. + """ + + def __init__(self, progress_update, total_lines, first_line): + self.progress_update = progress_update + self.first_line = first_line + + self.progress_percent = total_lines / 100.0 + logger.debug('self.progress_percent: %f', self.progress_percent) + + + def emit(self): + """ + Call this methid in a target function to update a progress value + based on a currently executed line number. + """ + + line = inspect.getframeinfo(inspect.stack()[1][0]).lineno + logger.debug('line: %d', line) + progress_value = (line - self.first_line) / self.progress_percent + logger.debug('progress_value: %d', progress_value) + + self.progress_update.emit(int(progress_value)) diff --git a/src/RepairGUIAdv/locate_subshapes.py b/src/RepairGUIAdv/locate_subshapes.py new file mode 100644 index 000000000..e73e3b3dd --- /dev/null +++ b/src/RepairGUIAdv/locate_subshapes.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import sys + +from qtsalome import QGridLayout, QFrame, QApplication + +from salome.geom.geomrepairadv.basedlg import BaseDlg +import GEOM + +class LocateSubShapesDlg(BaseDlg): + """ + Dialog for Locate Subshapes plugin that selects the sub-shapes of a compound + by length, area or volume depending on whether it is an EDGE, a FACE or a SOLID. + """ + def __init__(self, selection_level = GEOM.COMPOUND): + # Implement widget's content here + main_widget = QFrame() + layout = QGridLayout(main_widget) + layout.setContentsMargins(0, 0, 0, 0) + + BaseDlg.__init__( + self, main_widget, 'Locate Subshapes', 'locate_subshapes_algo.py', False, selection_level) + + +# For testing run as a module from geomrepairadv parent directory in +# Salome INSTALL, because the dialog needs a generated Ui_BaseDlg class +# that we don't have in the SOURCE. +# Example: +# $ python -m geomrepairadv.locate_subshapes +if __name__ == '__main__': + app = QApplication(sys.argv) + + dlg = LocateSubShapesDlg(None) + dlg.show() + + sys.exit(app.exec_()) diff --git a/src/RepairGUIAdv/merge_faces.py b/src/RepairGUIAdv/merge_faces.py new file mode 100644 index 000000000..cadd3e5c2 --- /dev/null +++ b/src/RepairGUIAdv/merge_faces.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import sys + +from qtsalome import QGridLayout, QFrame, QMessageBox, QApplication + +from libGEOM_Swig import GEOM_Swig +from salome.geom import geomBuilder +from .geomrepairadv_logger import logger +from .basedlg import BaseDlg +from .geomrepairadv_common import DlgRef_1Spin_QTD +import GEOM + +class MergeFacesDlg(BaseDlg): + """ + Dialog for Merge Faces plugin that merges selected faces with a given precision. + """ + + def __init__(self, selection_level = GEOM.FACE): + # Make layout for new widgets + main_widget = QFrame() + layout = QGridLayout(main_widget) + layout.setContentsMargins(0, 0, 0, 0) + + # Precision widget + self._precision_widget = DlgRef_1Spin_QTD() + self._precision_widget.TextLabel1.setText('Precision') + layout.addWidget(self._precision_widget, 0, 0) + + BaseDlg.__init__( + self, main_widget, 'Merge Faces', 'merge_faces_algo.py', True, selection_level) + + + def get_precision(self): + """ + Returns current precision value. + + Args: + None. + + Returns: + Double. + """ + + return self._precision_widget.SpinBox_DX.value() + + + def get_args(self): + """ + Collects arguments for a repair execution algorithm into a dictionary. + + Args: + None. + + Returns: + Dictionary with arguments for execution. + """ + + geom_swig = GEOM_Swig() + faces_ids = geom_swig.getLocalSelection() + logger.debug('faces_ids: %s', faces_ids) + + if len(faces_ids) < 2: + QMessageBox.warning( + None, + 'Warning', + 'The algorithm needs at least two selected faces!\nMerging was canceled.' + ) + return None + + # Get faces from a temporary copy object + builder = geomBuilder.New() + faces = builder.SubShapes(self._selected_copy, faces_ids) + logger.debug('faces: %s', faces) + + return { + 'face_a': faces[0], + 'face_b': faces[1], + 'result_name': self.get_result_name(), + 'precision': self.get_precision() + } + + +# For testing run as a module from geomrepairadv parent directory in +# Salome INSTALL, because the dialog needs a generated Ui_BaseDlg class +# that we don't have in the SOURCE. +# Example: +# $ python -m geomrepairadv.merge_faces +if __name__ == '__main__': + app = QApplication(sys.argv) + + dlg = MergeFacesDlg(None) + dlg.show() + + sys.exit(app.exec_()) diff --git a/src/RepairGUIAdv/merge_faces_algo.py b/src/RepairGUIAdv/merge_faces_algo.py new file mode 100755 index 000000000..7e77b82f0 --- /dev/null +++ b/src/RepairGUIAdv/merge_faces_algo.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +"""Example of algorithm script for GEOM Merge Faces plugin. +""" + +import sys +import logging +from time import sleep + +import salome + +from salome.geom import geomBuilder +from qtsalome import QFileDialog, QApplication, pyqtSignal + + +salome.salome_init() +geompy = geomBuilder.New() + + +def run(args_dict, progress_emitter): + """ + Helper function to call run() with arguments parsed from dictionary. + + Args: + args_dict - arguments as pairs string : any type value + + Returns: + A string with result description. + """ + + logging.info('Run Merge Faces algorithm.') + progress_emitter.emit() + + + if ('source_solid' not in args_dict or + 'face_a' not in args_dict or + 'face_b' not in args_dict or + 'result_name' not in args_dict): + + logging.info('Cant execute an algo because the arguments are empty!') + return False + + source_solid = args_dict['source_solid'] + face_a = args_dict['face_a'] + face_b = args_dict['face_b'] + result_name = args_dict['result_name'] + + + logging.info('Creating of two faces...') + progress_emitter.emit() + + # Fusion des deux faces + partition = geompy.MakePartition([face_a, face_b],[]) + points = [geompy.GetVertexNearPoint(partition, geompy.MakeVertex(-298, 29, 250)), + geompy.GetVertexNearPoint(partition, geompy.MakeVertex(178, 29, 250)), + geompy.GetVertexNearPoint(partition, geompy.MakeVertex(178, -282, 250)), + geompy.GetVertexNearPoint(partition, geompy.MakeVertex(-298, -282, 250))] + wire = geompy.MakePolyline(points,True) + fused_face = geompy.MakeFaceWires([wire], True) + geompy.addToStudy(fused_face, "fused_face") + + logging.info('Creating of a new geometry from the source brep...') + progress_emitter.emit() + + sleep(5) + + # Fusion des deux faces au sein de la boite + nettoyage de la boite + points = [geompy.GetVertexNearPoint(source_solid, geompy.MakeVertex(-298, 29, 250)), + geompy.GetVertexNearPoint(source_solid, geompy.MakeVertex(178, 29, 250)), + geompy.GetVertexNearPoint(source_solid, geompy.MakeVertex(178, -282, 250)), + geompy.GetVertexNearPoint(source_solid, geompy.MakeVertex(-298, -282, 250)), + geompy.GetVertexNearPoint(source_solid, geompy.MakeVertex(-298, 29, 0)), + geompy.GetVertexNearPoint(source_solid, geompy.MakeVertex(178, 29, 0)), + geompy.GetVertexNearPoint(source_solid, geompy.MakeVertex(178, -282, 0)), + geompy.GetVertexNearPoint(source_solid, geompy.MakeVertex(-298, -282, 0))] + # ### Fusion des deux faces + wire = geompy.MakePolyline(points[:4],True) + faces = [geompy.MakeFaceWires([wire], True)] + + logging.info('Cleaning of the new geometry...') + progress_emitter.emit() + + sleep(5) + + # Uncomment to simulate exception handling in a thread worker class + # raise Exception + + # ### Nettoyage des 4 faces latérales + wire = geompy.MakePolyline([points[3], points[2], points[6], points[7]],True) + faces.append(geompy.MakeFaceWires([wire], True)) + wire = geompy.MakePolyline([points[0], points[3], points[7], points[4]],True) + faces.append(geompy.MakeFaceWires([wire], True)) + wire = geompy.MakePolyline([points[1], points[0], points[4], points[5]],True) + faces.append(geompy.MakeFaceWires([wire], True)) + wire = geompy.MakePolyline([points[2], points[1], points[5], points[6]],True) + faces.append(geompy.MakeFaceWires([wire], True)) + + # ### Récupération de la dernière face + faces.append(geompy.GetFaceNearPoint(source_solid, geompy.MakeVertex(-59, -127, 0))) + + logging.info('Creating a solid...') + progress_emitter.emit() + + sleep(5) + + # ### Création du solide + shell = geompy.MakeShell(faces) + solid = geompy.MakeSolid(shell) + + geompy.addToStudy(solid, result_name) + + logging.info('Merge Faces algorithm was completed successfully.') + progress_emitter.emit() + + return True + + +def test(): + """ + Tests execution of repair algo script. + """ + + cube_file, _ = QFileDialog.getOpenFileName(None, 'Open brep', '/home', 'Brep Files (*.brep)') + if not cube_file: + return + + # cube_file = "PartitionCube.brep" + source_solid = geompy.ImportBREP(cube_file) + + # Récupération des faces à fusionner + face_a = geompy.GetFaceNearPoint(source_solid, geompy.MakeVertex(-143, -127, 250)) + face_b = geompy.GetFaceNearPoint(source_solid, geompy.MakeVertex(49,-127,250)) + + geompy.addToStudy(source_solid, "source_solid") + geompy.addToStudyInFather(source_solid, face_a, "face_a") + geompy.addToStudyInFather(source_solid, face_b, "face_b") + + + args_dict = { + 'source_solid': source_solid, + 'face_a': face_a, + 'face_b': face_b, + 'result_name': 'MergeFaces_result' + } + + # Dummy emitter + # TODO: doesn't work + progress_emitter = pyqtSignal() + + run(args_dict, progress_emitter) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + test() + sys.exit(app.exec_()) diff --git a/src/RepairGUIAdv/union_edges.py b/src/RepairGUIAdv/union_edges.py new file mode 100644 index 000000000..0dbd4776a --- /dev/null +++ b/src/RepairGUIAdv/union_edges.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2024 EDF +# +# This library 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. +# +# This library 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 this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# +# Author : Konstantin Leontev (OpenCascade S.A.S) + +import sys + +from qtsalome import QGridLayout, QFrame, QApplication + +from salome.geom.geomrepairadv.basedlg import BaseDlg +import GEOM + +class UnionEdgesDlg(BaseDlg): + """ + Dialog for Union Edges plugin that unifies edges of selected face. + """ + def __init__(self, selection_level = GEOM.COMPOUND): + # Implement widget's content here + main_widget = QFrame() + layout = QGridLayout(main_widget) + layout.setContentsMargins(0, 0, 0, 0) + + BaseDlg.__init__( + self, main_widget, 'Union Edges', 'union_edges_algo.py', False, selection_level) + + +# For testing run as a module from geomrepairadv parent directory in +# Salome INSTALL, because the dialog needs a generated Ui_BaseDlg class +# that we don't have in the SOURCE. +# Example: +# $ python -m geomrepairadv.union_edges +if __name__ == '__main__': + app = QApplication(sys.argv) + + dlg = UnionEdgesDlg(None) + dlg.show() + + sys.exit(app.exec_())