From cd334172908ccc4791a4c12f539eaebd0df42710 Mon Sep 17 00:00:00 2001 From: Konstantin Leontev Date: Wed, 27 Mar 2024 11:44:58 +0000 Subject: [PATCH] [bos #38044][EDF] (2023-T3) Support for automatic reparation. Base implementation of Sub-Shape dialog. --- src/RepairGUIAdv/CMakeLists.txt | 1 + src/RepairGUIAdv/basedlg.py | 29 ++- src/RepairGUIAdv/geomrepairadv_execute.py | 20 +- src/RepairGUIAdv/locate_subshapes.py | 246 ++++++++++++++++++-- src/RepairGUIAdv/locate_subshapes_algo.py | 42 ++-- src/RepairGUIAdv/locate_subshapes_limits.py | 112 +++++++++ 6 files changed, 396 insertions(+), 54 deletions(-) create mode 100755 src/RepairGUIAdv/locate_subshapes_limits.py diff --git a/src/RepairGUIAdv/CMakeLists.txt b/src/RepairGUIAdv/CMakeLists.txt index 1ef411c4f..efd485fa3 100644 --- a/src/RepairGUIAdv/CMakeLists.txt +++ b/src/RepairGUIAdv/CMakeLists.txt @@ -37,6 +37,7 @@ IF(SALOME_BUILD_GUI) geomrepairadv_worker.py locate_subshapes.py locate_subshapes_algo.py + locate_subshapes_limits.py merge_faces.py merge_faces_algo.py union_edges.py diff --git a/src/RepairGUIAdv/basedlg.py b/src/RepairGUIAdv/basedlg.py index 484401886..2edaeeda6 100644 --- a/src/RepairGUIAdv/basedlg.py +++ b/src/RepairGUIAdv/basedlg.py @@ -120,8 +120,7 @@ class BaseDlg(Ui_BaseDlg, QWidget): # Execution module # Name of particular algo module for each repair class - self._algo_name = '' - self.set_algoname(algo_name, is_default_location) + 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. @@ -130,7 +129,6 @@ class BaseDlg(Ui_BaseDlg, QWidget): # Default selection level self._selection_level = selection_level self._is_level_changed = False - self._is_local_selection = False # Connect selection manager salome_pyqt = SalomePyQt.SalomePyQt() @@ -219,9 +217,9 @@ class BaseDlg(Ui_BaseDlg, QWidget): if is_default_location: package_dir = Path(__file__).parent.absolute() - self._algo_name = package_dir.joinpath(algo_name) + return package_dir.joinpath(algo_name) else: - self._algo_name = algo_name + return algo_name def set_result_name(self, name): @@ -252,6 +250,20 @@ class BaseDlg(Ui_BaseDlg, QWidget): return self._result_widget.LineEdit1.text() + def get_selection_level(self): + """ + Return current selection level. + + Args: + None. + + Returns: + Selection level. + """ + + return self._selection_level + + def set_selection_level(self, selection_level): """ Sets selection level. @@ -296,23 +308,18 @@ class BaseDlg(Ui_BaseDlg, QWidget): # Init it again if the level was changed if self._is_level_changed: geom_swig.closeLocalSelection() - self._is_local_selection = False - # We need to init a local selection only once - if not self._is_local_selection: # Init it here sel_level = geomBuilder.EnumToLong(self._selection_level) geom_swig.initLocalSelection(entry, sel_level) - - self._is_local_selection = True else: # No entry - no local selection geom_swig.closeLocalSelection() - self._is_local_selection = False # We don't need the flag after selection was set self._is_level_changed = False + def on_select_object(self): """ Adds selected object to a dialog. diff --git a/src/RepairGUIAdv/geomrepairadv_execute.py b/src/RepairGUIAdv/geomrepairadv_execute.py index 9b76c58d5..86558e656 100644 --- a/src/RepairGUIAdv/geomrepairadv_execute.py +++ b/src/RepairGUIAdv/geomrepairadv_execute.py @@ -85,14 +85,15 @@ def module_from_filename(filename): return module -def execute(selected_object, algo_name, args_dict): +def execute(selected_object, algo_name, args_dict, is_dump_on = True): """ Executes GEOM advanced repair algorithm. Args: selected_object - geom object selected by user for algorithm algo_name - path to the algo module - args_dict - dictionary with arguments those are specific for each algo. + args_dict - dictionary with arguments those are specific for each algo + is_dump_on - if True saves the call to the Python dump. Returns: Result GEOM object or None if failed or canceled. @@ -136,13 +137,14 @@ def execute(selected_object, algo_name, args_dict): logger.error('Could not get a result object after exec of %s file!', str(algo_name)) return None - geompy.FuncToPythonDump( - selected_object, - result_object, - 'from salome.geom.geomrepairadv import geomrepairadv_execute\n', - 'geomrepairadv_execute.execute', - '\'' + str(algo_name) + '\', ' + args_dict_str - ) + if is_dump_on: + geompy.FuncToPythonDump( + selected_object, + result_object, + 'from salome.geom.geomrepairadv import geomrepairadv_execute\n', + 'geomrepairadv_execute.execute', + '\'' + str(algo_name) + '\', ' + args_dict_str + ) return result_object diff --git a/src/RepairGUIAdv/locate_subshapes.py b/src/RepairGUIAdv/locate_subshapes.py index 21cc090a5..9aeacc6b4 100644 --- a/src/RepairGUIAdv/locate_subshapes.py +++ b/src/RepairGUIAdv/locate_subshapes.py @@ -25,8 +25,10 @@ from qtsalome import QGridLayout, QFrame, QApplication, \ QComboBox, QLabel, QPushButton, QMessageBox from salome.geom.geomrepairadv.basedlg import BaseDlg +from salome.geom import geomBuilder from .geomrepairadv_common import DlgRef_1Spin_QTD +from .geomrepairadv_execute import execute import GEOM class LocateSubShapesDlg(BaseDlg): @@ -34,7 +36,13 @@ 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. """ + + SUBSHAPES_LABEL_TEXT = 'Sub-shapes: ' + def __init__(self, selection_level = GEOM.EDGE): + # Path to Min/max script + self._minmax_algo = self.set_algoname('locate_subshapes_limits.py', True) + # Implement widget's content here main_widget = QFrame() layout = QGridLayout(main_widget) @@ -48,11 +56,22 @@ class LocateSubShapesDlg(BaseDlg): self._type_widget.setToolTip('Select a type of shape measurement') self._type_widget.currentIndexChanged.connect(self.on_measurment_type_changed) + # Add Min/Max button + self._minmax_button = QPushButton("Compute Min/Max") + self._minmax_button.clicked.connect(self.on_minmax_button_clicked) + # Min/max values widgets decimals = 2 max_value = sys.float_info.max self._min_widget = DlgRef_1Spin_QTD('Min', 0, decimals, max_value) - self._max_widget = DlgRef_1Spin_QTD('Max', 100, decimals, max_value) + self._max_widget = DlgRef_1Spin_QTD('Max', 1000, decimals, max_value) + self._min_widget.SpinBox_DX.valueChanged.connect(self.on_limit_changed) + self._max_widget.SpinBox_DX.valueChanged.connect(self.on_limit_changed) + + # Sub-shapes number + self._subshapes_selected = 0 + self._subshapes_total = 0 + self._subshapes_label = QLabel() # Add select button self._select_button = QPushButton("Show Selected Sub-shapes") @@ -60,10 +79,12 @@ class LocateSubShapesDlg(BaseDlg): # Add the widgets to layout layout.addWidget(type_label, 0, 0) - layout.addWidget(self._type_widget, 1, 0) - layout.addWidget(self._min_widget, 2, 0) - layout.addWidget(self._max_widget, 3, 0) - layout.addWidget(self._select_button, 4, 0) + layout.addWidget(self._minmax_button, 1, 0) + layout.addWidget(self._type_widget, 2, 0) + layout.addWidget(self._min_widget, 3, 0) + layout.addWidget(self._max_widget, 4, 0) + layout.addWidget(self._subshapes_label, 5, 0) + layout.addWidget(self._select_button, 6, 0) # Init base dialog BaseDlg.__init__( @@ -91,6 +112,21 @@ class LocateSubShapesDlg(BaseDlg): self._max_widget.SpinBox_DX.value()] + def set_limits(self, min_value, max_value): + """ + Sets given values for min/max limits. + + Args: + None. + + Returns: + None. + """ + + self._min_widget.SpinBox_DX.setValue(min_value) + self._max_widget.SpinBox_DX.setValue(max_value) + + def get_measurment_type(self, index): """ Returns selection level based on current measurment type. @@ -111,6 +147,62 @@ class LocateSubShapesDlg(BaseDlg): return measurment_types[index] + def set_subshapes_counters(self, selected, total): + """ + Set counters for selected and total subshapes. + + Args: + None. + + Returns: + None. + """ + + self._subshapes_selected = selected + self._subshapes_total = total + + + def update_subshapes_label(self): + """ + Updates a text of Sub-Shapes label. + + Args: + None. + + Returns: + None. + """ + + selected = str(self._subshapes_selected) + total = str(self._subshapes_total) + + self._subshapes_label.setText(self.SUBSHAPES_LABEL_TEXT + selected + '/' + total) + + + def update_subshapes_info(self): + """ + Updates all info about Sub-Shapes in the dialog. + + Args: + None. + + Returns: + None. + """ + + if not self._selected_object: + return + + # Update counters + geompy = geomBuilder.New() + all_ids = geompy.SubShapeAllIDs(self._selected_object, self.get_selection_level()) + selected_ids = self.get_local_selection() + self.set_subshapes_counters(len(selected_ids), len(all_ids)) + + # Update label + self.update_subshapes_label() + + def on_measurment_type_changed(self, index): """ Changes selection level on type changed. @@ -128,6 +220,78 @@ class LocateSubShapesDlg(BaseDlg): # Clear pre selected sub-shapes list self.on_select_subshape() + self.update_subshapes_info() + + + def select_subshapes_in_limits(self): + """ + Updates a text of Sub-Shapes label. + + Args: + None. + + Returns: + None. + """ + + if not self._selected_object: + return + + # Get all sub-shapes + geompy = geomBuilder.New() + selection_level = self.get_selection_level() + subshapes_ids = geompy.SubShapeAllIDs(self._selected_object, selection_level) + + # Iterate over ids to check if it fits to limits + # TODO: implement selections + limits = self.get_limits() + for id in subshapes_ids: + # Get a sub-shape by id + pass + + # Get related parameter to check it later + param = None + if selection_level == GEOM.EDGE: + # Get a lenght of an edge + pass + elif selection_level == GEOM.FACE: + # Get an area of a face + pass + elif selection_level == GEOM.SOLID: + # Get a volume of a solid + pass + else: + # We shouldn't fall here + QMessageBox.warning( + None, 'Warning', 'Wrong selection level: %s!' % (selection_level)) + return + + # Check if it fits to the limits + if param >= limits[0] and param <= limits[1]: + # Select sub-shape + pass + else: + # Deselect sub-shape + pass + + # Update displayed info + self.update_subshapes_info() + + + def on_limit_changed(self): + """ + One of the limits was changed. + + Args: + None. + + Returns: + None. + """ + + # TODO: Do we need an interactive change here? + # self.select_subshapes_in_limits() + def on_select_button_clicked(self): """ @@ -140,9 +304,40 @@ class LocateSubShapesDlg(BaseDlg): None. """ - #TODO: what are we going to do on this click? - # Should it do a separated script? - QMessageBox.warning(None, 'Warning', 'Not implemented yet') + # Doesn't make any sence without selected object + if not self._selected_object: + QMessageBox.warning( + None, 'Warning', 'You must select an object to see sub-shapes selected!') + return + + self.select_subshapes_in_limits() + + + def on_minmax_button_clicked(self): + """ + Compute Min/Max limits on button click. + + Args: + None. + + Returns: + None. + """ + + # Doesn't make any sence without selected object + if not self._selected_object: + QMessageBox.warning(None, 'Warning', 'You must select an object to compute!') + return + + # Execute a separated script the same way as it is expected for on_apply() but without dump + args = { + 'result_name': 'dummy', + 'selection_level': self.get_selection_level() + } + + limits = execute(self._selected_object, self._minmax_algo, args, False) + if len(limits) >= 2: + self.set_limits(limits[0], limits[1]) def get_args(self): @@ -156,24 +351,45 @@ class LocateSubShapesDlg(BaseDlg): Dictionary with arguments for execution. """ + # Update selection with a current values + # TODO: should we call it here? + # In a worst case scenario we can run it twice + # if a user has just pressed selection button. + self.select_subshapes_in_limits() + + # Collect current values for the execution selected_ids = self.get_local_selection() - current_index = self._type_widget.currentIndex() - selection_level = self.get_measurment_type(current_index) - limits = self.get_limits() - min_selected = 1 + selection_level = self.get_selection_level() + min_selected = 0 if self.is_selection_valid(selected_ids, min_selected): return { 'selected_ids': selected_ids, 'result_name': self.get_result_name(), - 'selection_level': selection_level, - 'min_limit': limits[0], - 'max_limit': limits[1] + 'selection_level': selection_level } return None + def on_select_object(self): + """ + Override parent's method to display sub-shapes info. + + Args: + None. + + Returns: + None. + """ + + # Call parent method first + super().on_select_object() + + # Update displayed info + self.update_subshapes_info() + + # 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. diff --git a/src/RepairGUIAdv/locate_subshapes_algo.py b/src/RepairGUIAdv/locate_subshapes_algo.py index 959188190..bba7c30cc 100755 --- a/src/RepairGUIAdv/locate_subshapes_algo.py +++ b/src/RepairGUIAdv/locate_subshapes_algo.py @@ -55,9 +55,7 @@ def run(args_dict, progress_emitter): if ('source_solid' not in args_dict or 'selected_ids' not in args_dict or 'result_name' not in args_dict or - 'selection_level' not in args_dict or - 'min_limit' not in args_dict or - 'max_limit' not in args_dict): + 'selection_level' not in args_dict): logging.info('Cant execute an algo because the arguments are empty!') return False @@ -66,8 +64,6 @@ def run(args_dict, progress_emitter): selected_ids = args_dict['selected_ids'] result_name = args_dict['result_name'] selection_level = args_dict['selection_level'] - min_limit = args_dict['min_limit'] - max_limit = args_dict['max_limit'] # Replace the lines below with an actual algorithm logging.info('Received arguments:') @@ -75,21 +71,29 @@ def run(args_dict, progress_emitter): logging.info('\tselected_ids: %s', selected_ids) logging.info('\tresult_name: %s', result_name) logging.info('\tselection_level: %s', selection_level) - logging.info('\tmin_limit: %s', min_limit) - logging.info('\tmax_limit: %s', max_limit) - progress_emitter.emit() - - sleep(1) - - logging.warning('The algo script is not implemented! Return the copy of the source object...') - solid = geompy.MakeCopy(source_solid, result_name) progress_emitter.emit() - logging.info('Done.') + sleep(0.1) + + # Make a group + group = geompy.CreateGroup(source_solid, selection_level, theName = result_name) + + # Iterate all over the group's ids and remove unselected + group_ids = geompy.GetObjectIDs(group) + logging.info('Group Sub-shapes ids: %s', group_ids) + + for subshape_id in group_ids: + if subshape_id not in selected_ids: + geompy.RemoveObject(group, subshape_id) + logging.info('\tSub-shape %s was removed!', subshape_id) + progress_emitter.emit() - return solid + logging.info('Group of selected sub-shapes was created.') + progress_emitter.emit() + + return group def test(): @@ -107,17 +111,17 @@ def test(): source_solid = geompy.ImportBREP(test_file) geompy.addToStudy(source_solid, "source_solid") + selection_level = GEOM.EDGE + # TODO: Implement for actual algorithm # Here we just use all ids. - all_subshapes = geompy.SubShapeAllIDs(source_solid, GEOM.EDGE) + all_subshapes = geompy.SubShapeAllIDs(source_solid, selection_level) args_dict = { 'source_solid': source_solid, 'selected_ids': all_subshapes, 'result_name': 'LocateSubshapes_result', - 'selection_level': GEOM.EDGE, - 'min_limit': 0.0, - 'max_limit': 99.99 + 'selection_level': selection_level } # Dummy emitter diff --git a/src/RepairGUIAdv/locate_subshapes_limits.py b/src/RepairGUIAdv/locate_subshapes_limits.py new file mode 100755 index 000000000..a65f84083 --- /dev/null +++ b/src/RepairGUIAdv/locate_subshapes_limits.py @@ -0,0 +1,112 @@ +# -*- 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 Locate Subshapes plugin. +""" + +import sys +import logging +from time import sleep + +import salome + +from salome.geom import geomBuilder +from qtsalome import QFileDialog, QApplication, pyqtSignal +import GEOM + + +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 result object. + """ + + logging.info('Run Locate Subshapes algorithm.') + progress_emitter.emit() + + + if ('source_solid' not in args_dict or + 'selection_level' not in args_dict): + + logging.info('Cant execute an algo because the arguments are empty!') + return False + + source_solid = args_dict['source_solid'] + selection_level = args_dict['selection_level'] + + # Replace the lines below with an actual algorithm + logging.info('Received arguments:') + logging.info('\tsource_solid: %s', source_solid) + logging.info('\tselection_level: %s', selection_level) + progress_emitter.emit() + + sleep(1) + + logging.warning('The algo script is not implemented! Return default values...') + limits = [0.0, 100.0] + + logging.info('Done.') + progress_emitter.emit() + + return limits + + +def test(): + """ + Tests execution of repair algo script. + """ + + logging.basicConfig(level=logging.DEBUG) + + test_file, _ = QFileDialog.getOpenFileName(None, 'Open brep', '/home', 'Brep Files (*.brep)') + if not test_file: + return + + # test_file = "PartitionCube.brep" + source_solid = geompy.ImportBREP(test_file) + geompy.addToStudy(source_solid, "source_solid") + + args_dict = { + 'source_solid': source_solid, + 'selection_level': GEOM.EDGE + } + + # Dummy emitter + # TODO: doesn't work + # progress_emitter = pyqtSignal() + progress_emitter = type('DummyEmitter', (object,), {'emit': lambda self: sleep(0.1)})() + + run(args_dict, progress_emitter) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + test() + sys.exit(app.exec_())