]> SALOME platform Git repositories - modules/geom.git/commitdiff
Salome HOME
[bos #38044][EDF] (2023-T3) Support for automatic reparation. Added execution chain...
authorKonstantin Leontev <Konstantin.LEONTEV@opencascade.com>
Tue, 9 Jan 2024 12:25:23 +0000 (12:25 +0000)
committerKonstantin Leontev <Konstantin.LEONTEV@opencascade.com>
Fri, 14 Jun 2024 16:57:46 +0000 (17:57 +0100)
15 files changed:
src/CMakeLists.txt
src/RepairGUIAdv/CMakeLists.txt [new file with mode: 0644]
src/RepairGUIAdv/DlgRef_1Spin_QTD.ui [new file with mode: 0644]
src/RepairGUIAdv/basedlg.py [new file with mode: 0644]
src/RepairGUIAdv/basedlg.ui [new file with mode: 0644]
src/RepairGUIAdv/geomrepairadv_common.py [new file with mode: 0644]
src/RepairGUIAdv/geomrepairadv_execute.py [new file with mode: 0644]
src/RepairGUIAdv/geomrepairadv_logger.py [new file with mode: 0644]
src/RepairGUIAdv/geomrepairadv_plugins.py [new file with mode: 0644]
src/RepairGUIAdv/geomrepairadv_progress.py [new file with mode: 0644]
src/RepairGUIAdv/geomrepairadv_worker.py [new file with mode: 0644]
src/RepairGUIAdv/locate_subshapes.py [new file with mode: 0644]
src/RepairGUIAdv/merge_faces.py [new file with mode: 0644]
src/RepairGUIAdv/merge_faces_algo.py [new file with mode: 0755]
src/RepairGUIAdv/union_edges.py [new file with mode: 0644]

index 4399dd140154932d5b322b26e14c86addc8ff2ee..5f9654ef9455b0c99673543134f0e31022ab86db 100644 (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
   )
 
 ##
diff --git a/src/RepairGUIAdv/CMakeLists.txt b/src/RepairGUIAdv/CMakeLists.txt
new file mode 100644 (file)
index 0000000..d2251be
--- /dev/null
@@ -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 (file)
index 0000000..da50908
--- /dev/null
@@ -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>
diff --git a/src/RepairGUIAdv/basedlg.py b/src/RepairGUIAdv/basedlg.py
new file mode 100644 (file)
index 0000000..26524ca
--- /dev/null
@@ -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 (file)
index 0000000..6f82f20
--- /dev/null
@@ -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>
diff --git a/src/RepairGUIAdv/geomrepairadv_common.py b/src/RepairGUIAdv/geomrepairadv_common.py
new file mode 100644 (file)
index 0000000..06e91a0
--- /dev/null
@@ -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 (file)
index 0000000..04e488a
--- /dev/null
@@ -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 (file)
index 0000000..b3f877c
--- /dev/null
@@ -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 (file)
index 0000000..ddf58ba
--- /dev/null
@@ -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 (file)
index 0000000..8acd468
--- /dev/null
@@ -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 (file)
index 0000000..2a0acaf
--- /dev/null
@@ -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 (file)
index 0000000..e73e3b3
--- /dev/null
@@ -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 (file)
index 0000000..cadd3e5
--- /dev/null
@@ -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 (executable)
index 0000000..7e77b82
--- /dev/null
@@ -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 (file)
index 0000000..0dbd477
--- /dev/null
@@ -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_())