# *************************************************************************** # * Copyright (c) 2017 Markus Hovorka * # * Copyright (c) 2018 Bernd Hahnebach * # * * # * This file is part of the FreeCAD CAx development system. * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program 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 Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** __title__ = "FreeCAD FEM select widget" __author__ = "Markus Hovorka, Bernd Hahnebach" __url__ = "https://www.freecad.org" ## @package FemSelectWidget # \ingroup FEM # \brief FreeCAD FEM FemSelectWidget from typing import List, TYPE_CHECKING from PySide import QtGui from PySide import QtCore import FreeCAD import FreeCADGui import FreeCADGui as Gui from femtools import geomtools from femguiutils.disambiguate_solid_selection import disambiguate_solid_selection if TYPE_CHECKING: from Part import Face, Edge, PartFeature def solids_with_edge(parent_part: "PartFeature", edge: "Edge") -> List[int]: """ Return the indices in the parent's list of solids that are partially bounded by edge. """ solids_with_edge: List[int] = [] for idx, solid in enumerate(parent_part.Shape.Solids): if any([edge.isSame(e) for e in solid.Edges]): solids_with_edge.append(idx) return solids_with_edge def solids_with_face(parent_part: "PartFeature", face: "Face") -> List[int]: """ Return the indices in the parent's list of solids that are partially bounded by face. """ solids_with_face: List[int] = [] for idx, solid in enumerate(parent_part.Shape.Solids): if any([face.isSame(f) for f in solid.Faces]): solids_with_face.append(idx) return solids_with_face class _Selector(QtGui.QWidget): def __init__(self): super().__init__() self._references = [] self._register = dict() addBtn = QtGui.QPushButton(self.tr("Add")) delBtn = QtGui.QPushButton(self.tr("Remove")) addBtn.clicked.connect(self._add) delBtn.clicked.connect(self._del) btnLayout = QtGui.QHBoxLayout() btnLayout.addWidget(addBtn) btnLayout.addWidget(delBtn) self._model = QtGui.QStandardItemModel() self._view = SmallListView() self._view.setModel(self._model) self._helpTextLbl = QtGui.QLabel() self._helpTextLbl.setWordWrap(True) mainLayout = QtGui.QVBoxLayout() mainLayout.addWidget(self._helpTextLbl) mainLayout.addLayout(btnLayout) mainLayout.addWidget(self._view) self.setLayout(mainLayout) def references(self): return [entry for entry in self._references if entry[1]] def setReferences(self, references): self._references = [] self._updateReferences(references) def setHelpText(self, text): self._helpTextLbl.setText(text) @QtCore.Slot() def _add(self): selection = self.getSelection() self._updateReferences(selection) @QtCore.Slot() def _del(self): selected = self._view.selectedIndexes() for index in selected: identifier = self._model.data(index) obj, sub = self._register[identifier] refIndex = self._getIndex(obj) entry = self._references[refIndex] newSub = tuple(x for x in entry[1] if x != sub) self._references[refIndex] = (obj, newSub) self._model.removeRow(index.row()) def _updateReferences(self, selection): for obj, subList in selection: index = self._getIndex(obj) for sub in subList: entry = self._references[index] if sub not in entry[1]: self._addToWidget(obj, sub) newEntry = (obj, entry[1] + (sub,)) self._references[index] = newEntry def _addToWidget(self, obj, sub): identifier = f"{obj.Name}::{sub}" item = QtGui.QStandardItem(identifier) self._model.appendRow(item) self._register[identifier] = (obj, sub) def _getIndex(self, obj): for i, entry in enumerate(self._references): if entry[0] == obj: return i self._references.append((obj, tuple())) return len(self._references) - 1 def getSelection(self): raise NotImplementedError() class BoundarySelector(_Selector): def __init__(self): super().__init__() self.setWindowTitle(self.tr("Select Faces/Edges/Vertexes")) self.setHelpText(self.tr('To add references: select them in the 3D view and click "Add".')) def getSelection(self): selection = [] for selObj in Gui.Selection.getSelectionEx(): if selObj.HasSubObjects: item = (selObj.Object, tuple(selObj.SubElementNames)) selection.append(item) return selection class SolidSelector(_Selector): def __init__(self): super().__init__() self.setWindowTitle(self.tr("Select Solids")) self.setHelpText( self.tr( "Select elements part of the solid that shall be added" ' to the list. To add the solid click "Add".' ) ) def getSelection(self): selection = [] for selObj in Gui.Selection.getSelectionEx(): solids = set() for sub in self._getObjects(selObj.Object, selObj.SubElementNames): s = self._getSolidOfSub(selObj.Object, sub) if s is not None: solids.add(s) if solids: item = (selObj.Object, tuple(solids)) selection.append(item) if len(selection) == 0: FreeCAD.Console.PrintMessage( "Object with no Shape selected or nothing selected at all.\n" ) return selection def _getObjects(self, obj, names): objects = [] if not hasattr(obj, "Shape"): FreeCAD.Console.PrintMessage("Selected object has no Shape.\n") return objects shape = obj.Shape for n in names: if n.startswith("Face"): objects.append(shape.Faces[int(n[4:]) - 1]) elif n.startswith("Edge"): objects.append(shape.Edges[int(n[4:]) - 1]) elif n.startswith("Vertex"): objects.append(shape.Vertexes[int(n[6:]) - 1]) elif n.startswith("Solid"): objects.append(shape.Solids[int(n[5:]) - 1]) return objects def _getSolidOfSub(self, obj, sub): foundSolids = set() if sub.ShapeType == "Solid": for solidId, solid in enumerate(obj.Shape.Solids): if sub.isSame(solid): foundSolids.add("Solid" + str(solidId + 1)) elif sub.ShapeType == "Face": for solidId, solid in enumerate(obj.Shape.Solids): if self._findSub(sub, solid.Faces): foundSolids.add("Solid" + str(solidId + 1)) elif sub.ShapeType == "Edge": for solidId, solid in enumerate(obj.Shape.Solids): if self._findSub(sub, solid.Edges): foundSolids.add("Solid" + str(solidId + 1)) elif sub.ShapeType == "Vertex": for solidId, solid in enumerate(obj.Shape.Solids): if self._findSub(sub, solid.Vertexes): foundSolids.add("Solid" + str(solidId + 1)) if len(foundSolids) == 1: it = iter(foundSolids) return next(it) return None def _findSub(self, sub, subList): for i, s in enumerate(subList): if s.isSame(sub): return True return False class SmallListView(QtGui.QListView): def sizeHint(self): return QtCore.QSize(50, 50) class GeometryElementsSelection(QtGui.QWidget): def __init__(self, ref, eltypes, multigeom, showHintEmptyList): super().__init__() # init ui stuff FreeCADGui.Selection.clearSelection() self.sel_server = None self.obj_notvisible = [] self.initElemTypes(eltypes) self.allow_multiple_geom_types = multigeom self.showHintEmptyList = showHintEmptyList self.initUI() # set references and fill the list widget self.references = [] if ref: self.tuplereferences = ref self.get_references() self.rebuild_list_References() def initElemTypes(self, eltypes): self.sel_elem_types = eltypes # FreeCAD.Console.PrintMessage( # "Selection of: {} is allowed.\n".format(self.sel_elem_types) # ) self.sel_elem_text = "" for e in self.sel_elem_types: self.sel_elem_text += e + ", " self.sel_elem_text = self.sel_elem_text.rstrip(", ") # FreeCAD.Console.PrintMessage("Selection of: " + self.sel_elem_text + " is allowed.\n") self.selection_mode_std_print_message = ( "Single click on a " + self.sel_elem_text + " will add it to the list" ) self.selection_mode_solid_print_message = ( "Single click on a Face or Edge which belongs " "to one Solid will add the Solid to the list" ) def initUI(self): # ArchPanel is coded without ui-file too # title self.setWindowTitle(self.tr("Geometry Reference Selector")) # button self.pushButton_Add = QtGui.QPushButton(self.tr("Add")) self.pushButton_Remove = QtGui.QPushButton(self.tr("Remove")) # label self.lb_help = QtGui.QLabel() self.lb_help.setWordWrap(True) selectHelpText = self.tr("Select geometry of type: {}{}{}").format( "", self.sel_elem_text, "" ) self.lb_help.setText(selectHelpText) # list self.list_References = QtGui.QListWidget() # radiobutton down the list self.lb_selmod = QtGui.QLabel() self.lb_selmod.setText(self.tr("Selection mode")) self.rb_standard = QtGui.QRadioButton(self.tr(self.sel_elem_text.lstrip("Solid, "))) self.rb_solid = QtGui.QRadioButton(self.tr("Solid")) # radio button layout rbtnLayout = QtGui.QHBoxLayout() rbtnLayout.addWidget(self.lb_selmod) rbtnLayout.addWidget(self.rb_standard) rbtnLayout.addWidget(self.rb_solid) # add/remove button subLayout = QtGui.QHBoxLayout() subLayout.addWidget(self.pushButton_Add) subLayout.addWidget(self.pushButton_Remove) # main layout mainLayout = QtGui.QVBoxLayout() mainLayout.addWidget(self.lb_help) mainLayout.addLayout(subLayout) mainLayout.addWidget(self.list_References) tip1 = self.tr( "Click and select geometric elements to add them to the list.{}" "The following geometry elements can be selected: {}{}{}" ).format("
", "", self.sel_elem_text, "") tip2 = self.tr( "{}If no geometry is added to the list, all remaining ones are used." ).format("
") tip1 += tip2 if self.showHintEmptyList else "" self.pushButton_Add.setToolTip(tip1) # if only "Solid" is avail, std-sel-mode is obsolete if "Solid" in self.sel_elem_types and len(self.sel_elem_types) == 1: self.selection_mode_solid = True else: self.selection_mode_solid = False # show radio buttons, if a solid and at least one nonsolid is allowed if "Solid" in self.sel_elem_types and len(self.sel_elem_types) > 1: self.rb_standard.setChecked(True) self.rb_solid.setChecked(False) mainLayout.addLayout(rbtnLayout) self.setLayout(mainLayout) # signals and slots self.list_References.itemSelectionChanged.connect(self.select_clicked_reference_shape) self.list_References.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.list_References.connect( self.list_References, QtCore.SIGNAL("customContextMenuRequested(QPoint)"), self.references_list_right_clicked, ) QtCore.QObject.connect(self.pushButton_Add, QtCore.SIGNAL("clicked()"), self.add_references) QtCore.QObject.connect( self.pushButton_Remove, QtCore.SIGNAL("clicked()"), self.remove_selected_reference ) QtCore.QObject.connect( self.rb_standard, QtCore.SIGNAL("toggled(bool)"), self.choose_selection_mode_standard ) QtCore.QObject.connect( self.rb_solid, QtCore.SIGNAL("toggled(bool)"), self.choose_selection_mode_solid ) def get_references(self): for ref in self.tuplereferences: for elem in ref[1]: self.references.append((ref[0], elem)) def get_item_text(self, ref): return ref[0].Name + ":" + ref[1] def get_allitems_text(self): items = [] for ref in self.references: items.append(self.get_item_text(ref)) return sorted(items) def rebuild_list_References(self, current_row=0): self.list_References.clear() for listItemName in self.get_allitems_text(): self.list_References.addItem(listItemName) if current_row > self.list_References.count() - 1: # first row is 0 current_row = self.list_References.count() - 1 if self.list_References.count() > 0: self.list_References.setCurrentItem(self.list_References.item(current_row)) def select_clicked_reference_shape(self): self.setback_listobj_visibility() if self.sel_server: FreeCADGui.Selection.removeObserver(self.sel_server) self.sel_server = None if not self.sel_server: if not self.references: return currentItemName = str(self.list_References.currentItem().text()) for ref in self.references: if self.get_item_text(ref) == currentItemName: # print("found: shape: " + ref[0].Name + " element: " + ref[1]) if not ref[0].ViewObject.Visibility: self.obj_notvisible.append(ref[0]) ref[0].ViewObject.Visibility = True FreeCADGui.Selection.clearSelection() ref_sh_type = ref[0].Shape.ShapeType if ref[1].startswith("Solid") and ( ref_sh_type == "Compound" or ref_sh_type == "CompSolid" ): # selection of Solids of Compounds or CompSolids is not possible # because a Solid is no Subelement # since only Subelements can be selected # we're going to select all Faces of said Solids # the method getElement(element)doesn't return Solid elements solid = geomtools.get_element(ref[0], ref[1]) if not solid: return faces = [] for fs in solid.Faces: # find these faces in ref[0] for i, fref in enumerate(ref[0].Shape.Faces): if fs.isSame(fref): fref_elstring = "Face" + str(i + 1) if fref_elstring not in faces: faces.append(fref_elstring) for f in faces: FreeCADGui.Selection.addSelection(ref[0], f) else: # Selection of all other element types is supported FreeCADGui.Selection.addSelection(ref[0], ref[1]) def setback_listobj_visibility(self): """set back Visibility of the list objects""" FreeCADGui.Selection.clearSelection() for obj in self.obj_notvisible: obj.ViewObject.Visibility = False self.obj_notvisible = [] def references_list_right_clicked(self, QPos): self.contextMenu = QtGui.QMenu() menu_item_remove_selected = self.contextMenu.addAction("Remove Selected Geometry") menu_item_remove_all = self.contextMenu.addAction("Clear List") if not self.references: menu_item_remove_selected.setDisabled(True) menu_item_remove_all.setDisabled(True) self.connect( menu_item_remove_selected, QtCore.SIGNAL("triggered()"), self.remove_selected_reference ) self.connect(menu_item_remove_all, QtCore.SIGNAL("triggered()"), self.remove_all_references) parentPosition = self.list_References.mapToGlobal(QtCore.QPoint(0, 0)) self.contextMenu.move(parentPosition + QPos) self.contextMenu.show() def remove_selected_reference(self): if not self.references: return currentItemName = str(self.list_References.currentItem().text()) currentRow = self.list_References.currentRow() for ref in self.references: if self.get_item_text(ref) == currentItemName: self.references.remove(ref) self.rebuild_list_References(currentRow) def remove_all_references(self): self.references = [] self.rebuild_list_References() def choose_selection_mode_standard(self, state): self.selection_mode_solid = not state if self.sel_server and not self.selection_mode_solid: FreeCAD.Console.PrintMessage(self.selection_mode_std_print_message + "\n") def choose_selection_mode_solid(self, state): self.selection_mode_solid = state if self.sel_server and self.selection_mode_solid: FreeCAD.Console.PrintMessage(self.selection_mode_solid_print_message + "\n") def add_references(self): """Called if Button add_reference is triggered""" # in constraints EditTaskPanel the selection is active as soon as the taskpanel is open # here the addReference button EditTaskPanel has to be triggered to start selection mode self.setback_listobj_visibility() FreeCADGui.Selection.clearSelection() # start SelectionObserver and parse the function to add the References to the widget if self.selection_mode_solid: # print message on button click print_message = self.selection_mode_solid_print_message else: print_message = self.selection_mode_std_print_message if not self.sel_server: # if we do not check, we would start a new SelectionObserver # on every click on addReference button # but close only one SelectionObserver on leaving the task panel self.sel_server = FemSelectionObserver(self.selectionParser, print_message) def selectionParser(self, selection): if hasattr(selection[0], "Shape") and selection[1]: FreeCAD.Console.PrintMessage( "Selection: {} {} {}\n".format( selection[0].Shape.ShapeType, selection[0].Name, selection[1] ) ) sobj = selection[0] elt = sobj.Shape.getElement(selection[1]) ele_ShapeType = elt.ShapeType if self.selection_mode_solid and "Solid" in self.sel_elem_types: # in solid selection mode use edges and faces for selection of a solid # adapt selection variable to hold the Solid solid_to_add = None if ele_ShapeType == "Edge": solid_indices = solids_with_edge(sobj, elt) elif ele_ShapeType == "Face": solid_indices = solids_with_face(sobj, elt) else: raise ValueError(f"Unexpected shape type: {ele_ShapeType}") if not solid_indices: raise ValueError( f"Selected {ele_ShapeType} does not appear to belong to any of the part's solids" ) elif len(solid_indices) == 1: solid_to_add = str(solid_indices[0] + 1) else: selected_solid = disambiguate_solid_selection(sobj, solid_indices) if selected_solid is not None: solid_to_add = selected_solid[len("Solid") :] if solid_to_add: selection = (sobj, "Solid" + solid_to_add) ele_ShapeType = "Solid" FreeCAD.Console.PrintMessage( " Selection variable adapted to hold the Solid: {} {} {}\n".format( sobj.Shape.ShapeType, sobj.Name, selection[1] ) ) else: return if ele_ShapeType in self.sel_elem_types: if ( self.selection_mode_solid and ele_ShapeType == "Solid" ) or self.selection_mode_solid is False: if selection not in self.references: # only equal shape types are allowed to add if self.allow_multiple_geom_types is False: if self.has_equal_references_shape_types(ele_ShapeType): self.references.append(selection) self.rebuild_list_References( self.get_allitems_text().index(self.get_item_text(selection)) ) else: # selected shape will not added to the list FreeCADGui.Selection.clearSelection() else: # multiple shape types are allowed to add self.references.append(selection) self.rebuild_list_References( self.get_allitems_text().index(self.get_item_text(selection)) ) else: # selected shape will not added to the list FreeCADGui.Selection.clearSelection() message = " Selection {} is in reference list already!\n".format( self.get_item_text(selection) ) FreeCAD.Console.PrintMessage(message) QtGui.QMessageBox.critical( None, "Geometry already in list", message.lstrip(" ") ) else: # selected shape will not added to the list FreeCADGui.Selection.clearSelection() message = ele_ShapeType + " is not allowed to add to the list!\n" FreeCAD.Console.PrintMessage(message) QtGui.QMessageBox.critical(None, "Wrong shape type", message) def has_equal_references_shape_types(self, ref_shty=""): for ref in self.references: # the method getElement(element) does not return Solid elements r = geomtools.get_element(ref[0], ref[1]) if not r: FreeCAD.Console.PrintError(f"Problem in retrieving element: {ref[1]} \n") continue FreeCAD.Console.PrintLog( " ReferenceShape : {}, {}, {} --> {}\n".format( r.ShapeType, ref[0].Name, ref[0].Label, ref[1] ) ) if not ref_shty: ref_shty = r.ShapeType if r.ShapeType != ref_shty: message = "Multiple shape types are not allowed in the reference list.\n" FreeCAD.Console.PrintMessage(message) QtGui.QMessageBox.critical(None, "Multiple ShapeTypes not allowed", message) return False return True def finish_selection(self): self.setback_listobj_visibility() if self.sel_server: FreeCADGui.Selection.removeObserver(self.sel_server) class FemSelectionObserver: """selection observer especially for the needs of geometry reference selection of FEM""" def __init__(self, parseSelectionFunction, print_message=""): self.parseSelectionFunction = parseSelectionFunction FreeCADGui.Selection.addObserver(self) # FreeCAD.Console.PrintMessage(print_message + "!\n") def addSelection(self, docName, objName, sub, pos): selected_object = FreeCAD.getDocument(docName).getObject(objName) # get the obj objName if FreeCADGui.editDocument().getInEdit().Object.Document != selected_object.Document: QtGui.QMessageBox.critical( None, "Selection error", "External object selection is not supported" ) FreeCADGui.Selection.clearSelection() return self.added_obj = (selected_object, sub) # on double click on a vertex of a solid sub is None and obj is the solid self.parseSelectionFunction(self.added_obj)