# *************************************************************************** # * 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__ = "http://www.freecadweb.org" ## @package FemSelectWidget # \ingroup FEM # \brief FreeCAD FEM FemSelectWidget import sys from PySide import QtGui from PySide import QtCore import FreeCAD import FreeCADGui import FreeCADGui as Gui from femtools import geomtools class _Selector(QtGui.QWidget): def __init__(self): super(_Selector, self).__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 = "%s::%s" % (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(BoundarySelector, self).__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(SolidSelector, self).__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) if sys.version_info.major >= 3: return next(it) else: return it.next() 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=True): super(GeometryElementsSelection, self).__init__() # init ui stuff FreeCADGui.Selection.clearSelection() self.selection_mode_solid = False self.sel_server = None self.obj_notvisible = [] self.initElemTypes(eltypes) self.allow_multiple_geom_types = multigeom # print(self.allow_multiple_geom_types) 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): # auch ArchPanel ist coded ohne ui-file # title self.setWindowTitle(self.tr("Geometry reference selector for a ") + self.sel_elem_text) # button self.pushButton_Add = QtGui.QPushButton(self.tr("Add")) # label self._helpTextLbl = QtGui.QLabel() self._helpTextLbl.setWordWrap(True) self._helpTextLbl.setText(self.tr( 'Click on "Add" and select geometric elements to add them to the list. ' "If no geometry is added to the list, all remaining ones are used. " "The following geometry elements are allowed to select: " ) + self.sel_elem_text) # 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")) self.rb_standard.setChecked(True) self.rb_solid.setChecked(False) # radio butoon layout rbtnLayout = QtGui.QHBoxLayout() rbtnLayout.addWidget(self.lb_selmod) rbtnLayout.addWidget(self.rb_standard) rbtnLayout.addWidget(self.rb_solid) # main layout mainLayout = QtGui.QVBoxLayout() mainLayout.addWidget(self._helpTextLbl) mainLayout.addWidget(self.pushButton_Add) mainLayout.addWidget(self.list_References) if "Solid" in self.sel_elem_types: 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.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): FreeCAD.Console.PrintMessage("Selection: {} {} {}\n".format( selection[0].Shape.ShapeType, selection[0].Name, selection[1] )) if hasattr(selection[0], "Shape") and 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": found_eltedge_in_other_solid = False for i, s in enumerate(sobj.Shape.Solids): for e in s.Edges: if elt.isSame(e): if found_eltedge_in_other_solid is False: solid_to_add = str(i + 1) else: # could be more than two solids, think of polar pattern FreeCAD.Console.PrintMessage( " Edge belongs to at least two solids: " " Solid{}, Solid{}\n" .format(solid_to_add, str(i + 1)) ) solid_to_add = None found_eltedge_in_other_solid = True elif ele_ShapeType == "Face": found_eltface_in_other_solid = False for i, s in enumerate(sobj.Shape.Solids): for e in s.Faces: if elt.isSame(e): if not found_eltface_in_other_solid: solid_to_add = str(i + 1) else: # AFAIK (bernd) a face can only belong to two solids FreeCAD.Console.PrintMessage( " Face belongs to two solids: Solid{}, Solid{}\n" .format(solid_to_add, str(i + 1)) ) solid_to_add = None found_eltface_in_other_solid = True 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( "Problem in retrieving element: {} \n".format(ref[1]) ) 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 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 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)