[bos #38044][EDF] (2023-T3) Support for automatic reparation. Added execution chain and generic GUI using merge faces plugin as an example.

This commit is contained in:
Konstantin Leontev 2024-01-09 12:25:23 +00:00 committed by DUC ANH HOANG
parent 5f12362860
commit 5995197696
15 changed files with 1656 additions and 0 deletions

View File

@ -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
)
##

View File

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

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DlgRef_1Spin_QTD</class>
<widget class="QWidget" name="DlgRef_1Spin_QTD">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>114</width>
<height>51</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QGridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QGroupBox" name="GroupBox1">
<property name="title">
<string/>
</property>
<layout class="QGridLayout">
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<property name="spacing">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="TextLabel1">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>TL1</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="SpinBox_DX"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<pixmapfunction>qPixmapFromMimeSource</pixmapfunction>
<resources/>
<connections/>
</ui>

332
src/RepairGUIAdv/basedlg.py Normal file
View File

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

111
src/RepairGUIAdv/basedlg.ui Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BaseDlg</class>
<widget class="QDialog" name="BaseDlg">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>411</width>
<height>278</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QGridLayout">
<item row="1" column="0">
<widget class="QGroupBox" name="GroupButtons">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string/>
</property>
<layout class="QHBoxLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item>
<widget class="QPushButton" name="buttonOk">
<property name="text">
<string>&amp;Apply and Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonApply">
<property name="text">
<string>&amp;Apply</string>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>91</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="buttonClose">
<property name="text">
<string>&amp;Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonHelp">
<property name="text">
<string>&amp;Help</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QFrame" name="child_placeholder">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>buttonOk</tabstop>
<tabstop>buttonApply</tabstop>
<tabstop>buttonClose</tabstop>
<tabstop>buttonHelp</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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