| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| __title__ = "FreeCAD FEM select widget" |
| __author__ = "Markus Hovorka, Bernd Hahnebach" |
| __url__ = "https://www.freecad.org" |
|
|
| |
| |
| |
|
|
| 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__() |
| |
| FreeCADGui.Selection.clearSelection() |
| self.sel_server = None |
| self.obj_notvisible = [] |
| self.initElemTypes(eltypes) |
| self.allow_multiple_geom_types = multigeom |
| self.showHintEmptyList = showHintEmptyList |
| self.initUI() |
| |
| self.references = [] |
| if ref: |
| self.tuplereferences = ref |
| self.get_references() |
| self.rebuild_list_References() |
|
|
| def initElemTypes(self, eltypes): |
| self.sel_elem_types = eltypes |
| |
| |
| |
| 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(", ") |
| |
| 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): |
| |
| |
| self.setWindowTitle(self.tr("Geometry Reference Selector")) |
| |
| self.pushButton_Add = QtGui.QPushButton(self.tr("Add")) |
| self.pushButton_Remove = QtGui.QPushButton(self.tr("Remove")) |
| |
| self.lb_help = QtGui.QLabel() |
| self.lb_help.setWordWrap(True) |
| selectHelpText = self.tr("Select geometry of type: {}{}{}").format( |
| "<b>", self.sel_elem_text, "</b>" |
| ) |
| self.lb_help.setText(selectHelpText) |
| |
| self.list_References = QtGui.QListWidget() |
| |
| 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")) |
| |
| rbtnLayout = QtGui.QHBoxLayout() |
| rbtnLayout.addWidget(self.lb_selmod) |
| rbtnLayout.addWidget(self.rb_standard) |
| rbtnLayout.addWidget(self.rb_solid) |
| |
| subLayout = QtGui.QHBoxLayout() |
| subLayout.addWidget(self.pushButton_Add) |
| subLayout.addWidget(self.pushButton_Remove) |
| |
| 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("<br>", "<b>", self.sel_elem_text, "</b>") |
| tip2 = self.tr( |
| "{}If no geometry is added to the list, all remaining ones are used." |
| ).format("<br>") |
| tip1 += tip2 if self.showHintEmptyList else "" |
| self.pushButton_Add.setToolTip(tip1) |
|
|
| |
| 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 |
|
|
| |
| 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) |
| |
| 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: |
| 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: |
| |
| 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" |
| ): |
| |
| |
| |
| |
| |
| solid = geomtools.get_element(ref[0], ref[1]) |
| if not solid: |
| return |
| faces = [] |
| for fs in solid.Faces: |
| |
| 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: |
| |
| 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""" |
| |
| |
| self.setback_listobj_visibility() |
| FreeCADGui.Selection.clearSelection() |
| |
| if self.selection_mode_solid: |
| print_message = self.selection_mode_solid_print_message |
| else: |
| print_message = self.selection_mode_std_print_message |
| if not self.sel_server: |
| |
| |
| |
| 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: |
| |
| |
| 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: |
| |
| 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: |
| |
| FreeCADGui.Selection.clearSelection() |
| else: |
| self.references.append(selection) |
| self.rebuild_list_References( |
| self.get_allitems_text().index(self.get_item_text(selection)) |
| ) |
| else: |
| |
| 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: |
| |
| 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: |
| |
| 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) |
| |
|
|
| def addSelection(self, docName, objName, sub, pos): |
| selected_object = FreeCAD.getDocument(docName).getObject(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) |
| |
| self.parseSelectionFunction(self.added_obj) |
|
|