[bos #38044][EDF] (2023-T3) Support for automatic reparation. Base implementation of Sub-Shape dialog.

This commit is contained in:
Konstantin Leontev 2024-03-27 11:44:58 +00:00
parent 8ef86c6185
commit cd33417290
6 changed files with 396 additions and 54 deletions

View File

@ -37,6 +37,7 @@ IF(SALOME_BUILD_GUI)
geomrepairadv_worker.py geomrepairadv_worker.py
locate_subshapes.py locate_subshapes.py
locate_subshapes_algo.py locate_subshapes_algo.py
locate_subshapes_limits.py
merge_faces.py merge_faces.py
merge_faces_algo.py merge_faces_algo.py
union_edges.py union_edges.py

View File

@ -120,8 +120,7 @@ class BaseDlg(Ui_BaseDlg, QWidget):
# Execution module # Execution module
# Name of particular algo module for each repair class # Name of particular algo module for each repair class
self._algo_name = '' self._algo_name = self.set_algoname(algo_name, is_default_location)
self.set_algoname(algo_name, is_default_location)
# Let it be always on top of the application. # Let it be always on top of the application.
# We need it because this dialog will run without parent. # We need it because this dialog will run without parent.
@ -130,7 +129,6 @@ class BaseDlg(Ui_BaseDlg, QWidget):
# Default selection level # Default selection level
self._selection_level = selection_level self._selection_level = selection_level
self._is_level_changed = False self._is_level_changed = False
self._is_local_selection = False
# Connect selection manager # Connect selection manager
salome_pyqt = SalomePyQt.SalomePyQt() salome_pyqt = SalomePyQt.SalomePyQt()
@ -219,9 +217,9 @@ class BaseDlg(Ui_BaseDlg, QWidget):
if is_default_location: if is_default_location:
package_dir = Path(__file__).parent.absolute() package_dir = Path(__file__).parent.absolute()
self._algo_name = package_dir.joinpath(algo_name) return package_dir.joinpath(algo_name)
else: else:
self._algo_name = algo_name return algo_name
def set_result_name(self, name): def set_result_name(self, name):
@ -252,6 +250,20 @@ class BaseDlg(Ui_BaseDlg, QWidget):
return self._result_widget.LineEdit1.text() 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): def set_selection_level(self, selection_level):
""" """
Sets selection level. Sets selection level.
@ -296,23 +308,18 @@ class BaseDlg(Ui_BaseDlg, QWidget):
# Init it again if the level was changed # Init it again if the level was changed
if self._is_level_changed: if self._is_level_changed:
geom_swig.closeLocalSelection() 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 # Init it here
sel_level = geomBuilder.EnumToLong(self._selection_level) sel_level = geomBuilder.EnumToLong(self._selection_level)
geom_swig.initLocalSelection(entry, sel_level) geom_swig.initLocalSelection(entry, sel_level)
self._is_local_selection = True
else: else:
# No entry - no local selection # No entry - no local selection
geom_swig.closeLocalSelection() geom_swig.closeLocalSelection()
self._is_local_selection = False
# We don't need the flag after selection was set # We don't need the flag after selection was set
self._is_level_changed = False self._is_level_changed = False
def on_select_object(self): def on_select_object(self):
""" """
Adds selected object to a dialog. Adds selected object to a dialog.

View File

@ -85,14 +85,15 @@ def module_from_filename(filename):
return module 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. Executes GEOM advanced repair algorithm.
Args: Args:
selected_object - geom object selected by user for algorithm selected_object - geom object selected by user for algorithm
algo_name - path to the algo module 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: Returns:
Result GEOM object or None if failed or canceled. 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)) logger.error('Could not get a result object after exec of %s file!', str(algo_name))
return None return None
geompy.FuncToPythonDump( if is_dump_on:
selected_object, geompy.FuncToPythonDump(
result_object, selected_object,
'from salome.geom.geomrepairadv import geomrepairadv_execute\n', result_object,
'geomrepairadv_execute.execute', 'from salome.geom.geomrepairadv import geomrepairadv_execute\n',
'\'' + str(algo_name) + '\', ' + args_dict_str 'geomrepairadv_execute.execute',
) '\'' + str(algo_name) + '\', ' + args_dict_str
)
return result_object return result_object

View File

@ -25,8 +25,10 @@ from qtsalome import QGridLayout, QFrame, QApplication, \
QComboBox, QLabel, QPushButton, QMessageBox QComboBox, QLabel, QPushButton, QMessageBox
from salome.geom.geomrepairadv.basedlg import BaseDlg from salome.geom.geomrepairadv.basedlg import BaseDlg
from salome.geom import geomBuilder
from .geomrepairadv_common import DlgRef_1Spin_QTD from .geomrepairadv_common import DlgRef_1Spin_QTD
from .geomrepairadv_execute import execute
import GEOM import GEOM
class LocateSubShapesDlg(BaseDlg): class LocateSubShapesDlg(BaseDlg):
@ -34,7 +36,13 @@ class LocateSubShapesDlg(BaseDlg):
Dialog for Locate Subshapes plugin that selects the sub-shapes of a compound 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. 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): 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 # Implement widget's content here
main_widget = QFrame() main_widget = QFrame()
layout = QGridLayout(main_widget) layout = QGridLayout(main_widget)
@ -48,11 +56,22 @@ class LocateSubShapesDlg(BaseDlg):
self._type_widget.setToolTip('Select a type of shape measurement') self._type_widget.setToolTip('Select a type of shape measurement')
self._type_widget.currentIndexChanged.connect(self.on_measurment_type_changed) 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 # Min/max values widgets
decimals = 2 decimals = 2
max_value = sys.float_info.max max_value = sys.float_info.max
self._min_widget = DlgRef_1Spin_QTD('Min', 0, decimals, max_value) 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 # Add select button
self._select_button = QPushButton("Show Selected Sub-shapes") self._select_button = QPushButton("Show Selected Sub-shapes")
@ -60,10 +79,12 @@ class LocateSubShapesDlg(BaseDlg):
# Add the widgets to layout # Add the widgets to layout
layout.addWidget(type_label, 0, 0) layout.addWidget(type_label, 0, 0)
layout.addWidget(self._type_widget, 1, 0) layout.addWidget(self._minmax_button, 1, 0)
layout.addWidget(self._min_widget, 2, 0) layout.addWidget(self._type_widget, 2, 0)
layout.addWidget(self._max_widget, 3, 0) layout.addWidget(self._min_widget, 3, 0)
layout.addWidget(self._select_button, 4, 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 # Init base dialog
BaseDlg.__init__( BaseDlg.__init__(
@ -91,6 +112,21 @@ class LocateSubShapesDlg(BaseDlg):
self._max_widget.SpinBox_DX.value()] 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): def get_measurment_type(self, index):
""" """
Returns selection level based on current measurment type. Returns selection level based on current measurment type.
@ -111,6 +147,62 @@ class LocateSubShapesDlg(BaseDlg):
return measurment_types[index] 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): def on_measurment_type_changed(self, index):
""" """
Changes selection level on type changed. Changes selection level on type changed.
@ -128,6 +220,78 @@ class LocateSubShapesDlg(BaseDlg):
# Clear pre selected sub-shapes list # Clear pre selected sub-shapes list
self.on_select_subshape() 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): def on_select_button_clicked(self):
""" """
@ -140,9 +304,40 @@ class LocateSubShapesDlg(BaseDlg):
None. None.
""" """
#TODO: what are we going to do on this click? # Doesn't make any sence without selected object
# Should it do a separated script? if not self._selected_object:
QMessageBox.warning(None, 'Warning', 'Not implemented yet') 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): def get_args(self):
@ -156,24 +351,45 @@ class LocateSubShapesDlg(BaseDlg):
Dictionary with arguments for execution. 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() selected_ids = self.get_local_selection()
current_index = self._type_widget.currentIndex() selection_level = self.get_selection_level()
selection_level = self.get_measurment_type(current_index) min_selected = 0
limits = self.get_limits()
min_selected = 1
if self.is_selection_valid(selected_ids, min_selected): if self.is_selection_valid(selected_ids, min_selected):
return { return {
'selected_ids': selected_ids, 'selected_ids': selected_ids,
'result_name': self.get_result_name(), 'result_name': self.get_result_name(),
'selection_level': selection_level, 'selection_level': selection_level
'min_limit': limits[0],
'max_limit': limits[1]
} }
return None 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 # For testing run as a module from geomrepairadv parent directory in
# Salome INSTALL, because the dialog needs a generated Ui_BaseDlg class # Salome INSTALL, because the dialog needs a generated Ui_BaseDlg class
# that we don't have in the SOURCE. # that we don't have in the SOURCE.

View File

@ -55,9 +55,7 @@ def run(args_dict, progress_emitter):
if ('source_solid' not in args_dict or if ('source_solid' not in args_dict or
'selected_ids' not in args_dict or 'selected_ids' not in args_dict or
'result_name' not in args_dict or 'result_name' not in args_dict or
'selection_level' not in args_dict or 'selection_level' not in args_dict):
'min_limit' not in args_dict or
'max_limit' not in args_dict):
logging.info('Cant execute an algo because the arguments are empty!') logging.info('Cant execute an algo because the arguments are empty!')
return False return False
@ -66,8 +64,6 @@ def run(args_dict, progress_emitter):
selected_ids = args_dict['selected_ids'] selected_ids = args_dict['selected_ids']
result_name = args_dict['result_name'] result_name = args_dict['result_name']
selection_level = args_dict['selection_level'] 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 # Replace the lines below with an actual algorithm
logging.info('Received arguments:') logging.info('Received arguments:')
@ -75,21 +71,29 @@ def run(args_dict, progress_emitter):
logging.info('\tselected_ids: %s', selected_ids) logging.info('\tselected_ids: %s', selected_ids)
logging.info('\tresult_name: %s', result_name) logging.info('\tresult_name: %s', result_name)
logging.info('\tselection_level: %s', selection_level) 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() 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() progress_emitter.emit()
return solid logging.info('Group of selected sub-shapes was created.')
progress_emitter.emit()
return group
def test(): def test():
@ -107,17 +111,17 @@ def test():
source_solid = geompy.ImportBREP(test_file) source_solid = geompy.ImportBREP(test_file)
geompy.addToStudy(source_solid, "source_solid") geompy.addToStudy(source_solid, "source_solid")
selection_level = GEOM.EDGE
# TODO: Implement for actual algorithm # TODO: Implement for actual algorithm
# Here we just use all ids. # Here we just use all ids.
all_subshapes = geompy.SubShapeAllIDs(source_solid, GEOM.EDGE) all_subshapes = geompy.SubShapeAllIDs(source_solid, selection_level)
args_dict = { args_dict = {
'source_solid': source_solid, 'source_solid': source_solid,
'selected_ids': all_subshapes, 'selected_ids': all_subshapes,
'result_name': 'LocateSubshapes_result', 'result_name': 'LocateSubshapes_result',
'selection_level': GEOM.EDGE, 'selection_level': selection_level
'min_limit': 0.0,
'max_limit': 99.99
} }
# Dummy emitter # Dummy emitter

View File

@ -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_())