Fem: Display popup menu when MeshGroup solid selection is ambiguous (#18812)

* Fem: Display popup menu when MeshGroup solid selection is ambiguous

* FEM: Update license to new language

---------

Co-authored-by: Chris Hennes <chennes@gmail.com>
This commit is contained in:
colinRawlings
2025-02-07 23:25:55 +01:00
committed by GitHub
parent 7ec76ffc9b
commit 293c8b0bc6
3 changed files with 239 additions and 29 deletions

View File

@@ -606,6 +606,7 @@ SET(FemGuiTests_SRCS
SET(FemGuiUtils_SRCS
femguiutils/__init__.py
femguiutils/disambiguate_solid_selection.py
femguiutils/migrate_gui.py
femguiutils/selection_widgets.py
)

View File

@@ -0,0 +1,191 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2024 Colin Rawlings <colin.d.rawlings@gmail.com> *
# * *
# * This file is part of FreeCAD. *
# * *
# * FreeCAD 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. *
# * *
# * FreeCAD 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 FreeCAD. If not, see *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
__title__ = "Disambiguate Solid Selection"
__author__ = "Colin Rawlings"
__url__ = "https://www.freecad.org"
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
import copy
from PySide import QtGui
from PySide import QtCore
highlight_color_t = List[Tuple[float, float, float, float]] # rgba value per face
highlight_map_t = Dict[Optional[str], highlight_color_t]
if TYPE_CHECKING:
from Part import Face, PartFeature
def calculate_unhighlighted_color(parent_part: "PartFeature") -> highlight_color_t:
"""
Cheap and cheerful function to dim object so that subsequent highlighting is apparent
"""
initial_color = parent_part.ViewObject.DiffuseColor
num_faces = len(parent_part.Shape.Faces)
if len(initial_color) == num_faces:
unhighlighted_color: highlight_color_t = initial_color
else:
unhighlighted_color: highlight_color_t = [initial_color[0]] * num_faces
for face_idx in range(num_faces):
rgba = unhighlighted_color[face_idx]
unhighlighted_color[face_idx] = (
rgba[0] * 0.5,
rgba[1] * 0.5,
rgba[2] * 0.5,
rgba[3] * 0.5,
)
return unhighlighted_color
class HighlightContext:
def __init__(self, parent_part: "PartFeature") -> None:
self._parent_part = parent_part
self._initial_color = parent_part.ViewObject.DiffuseColor
self._unhighlighted_color = calculate_unhighlighted_color(parent_part)
def _set_color(self, color: highlight_color_t) -> None:
self._parent_part.ViewObject.DiffuseColor = color
self._parent_part.ViewObject.update()
def __enter__(self) -> None:
self._set_color(self._unhighlighted_color)
def __exit__(self, *arg, **kwargs) -> bool:
self._set_color(self._initial_color)
return False
def parent_face_index(parent_part: "PartFeature", face: "Face") -> int:
"""
Return the index for the provided face in the parent_shape's
list of faces.
"""
return [face.isSame(f) for f in parent_part.Shape.Faces].index(True)
def solid_parent_faces_indices(parent_part: "PartFeature", solid_index: int) -> List[int]:
"""
Return the parent's face indices for the faces bounding
the given solid
"""
solid = parent_part.Shape.Solids[solid_index]
return [parent_face_index(parent_part, solid_face) for solid_face in solid.Faces]
def build_highlight_map(parent_part: "PartFeature", solid_indices: List[int]) -> highlight_map_t:
"""
Build a mapping from solid name to face colors for highlighting a given selected solid.
Indexing with None returns the unhighlighted coloring (see
calculate_unhighlighted_color).
"""
unhighlighted_color = calculate_unhighlighted_color(parent_part)
# build the highlighting map
highlight_colors_for_solid: highlight_map_t = {}
highlight_colors_for_solid[None] = unhighlighted_color
for idx in solid_indices:
solid_faces = solid_parent_faces_indices(parent_part, idx)
highlighted_colors = copy.deepcopy(unhighlighted_color)
for index in solid_faces:
rgba = unhighlighted_color[index]
highlighted_colors[index] = (rgba[0], rgba[1], 0.99, rgba[3])
highlight_colors_for_solid[solid_name_from_index(idx)] = highlighted_colors
return highlight_colors_for_solid
def solid_name_from_index(solid_index: int) -> str:
return f"Solid{solid_index+1}"
def disambiguate_solid_selection(
parent_part: "PartFeature", solid_indices: List[int]
) -> Optional[str]:
"""
@param solid_indices the indices which may be used to get a reference to the selected solid as
parent_part.Solids[index].
@return The name of the selected solid or None if the user cancels the selection
"""
# Build menu
menu_of_solids = QtGui.QMenu()
label = menu_of_solids.addAction(
"Selected entity belongs to multiple solids, please pick one ..."
)
label.setDisabled(True)
for index in solid_indices:
menu_of_solids.addAction(solid_name_from_index(index))
# Configure highlighting callbacks
last_action: list[QtGui.QAction] = []
highlight_colors_for_solid = build_highlight_map(parent_part, solid_indices)
def set_part_colors(solid_name: Optional[str]) -> None:
parent_part.ViewObject.DiffuseColor = highlight_colors_for_solid[solid_name]
parent_part.ViewObject.update()
def display_hover(action: QtGui.QAction) -> None:
if last_action and last_action[0].text() == action.text():
return
set_part_colors(action.text())
last_action.clear()
last_action.append(action)
def on_enter(arg__1: QtCore.QEvent) -> None:
if not last_action:
return
set_part_colors(last_action[0].text())
def on_leave(arg__1: QtCore.QEvent) -> None:
set_part_colors(None)
menu_of_solids.hovered.connect(display_hover) # type: ignore
menu_of_solids.enterEvent = on_enter
menu_of_solids.leaveEvent = on_leave
# Have user select desired solid
with HighlightContext(parent_part):
selected_action = menu_of_solids.exec_(QtGui.QCursor.pos())
# Process selection
if selected_action is None:
return None
return selected_action.text()

View File

@@ -30,6 +30,7 @@ __url__ = "https://www.freecad.org"
# \ingroup FEM
# \brief FreeCAD FEM FemSelectWidget
from typing import List, TYPE_CHECKING
from PySide import QtGui
from PySide import QtCore
@@ -38,6 +39,36 @@ 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):
@@ -468,36 +499,23 @@ class GeometryElementsSelection(QtGui.QWidget):
# 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
solid_indices = solids_with_edge(sobj, elt)
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
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"