Files
create/src/Mod/Fem/femguiutils/disambiguate_solid_selection.py
Colin Rawlings 3f4093aaed Add check that the solid_name is valid
Avoid errors when hovering on the menu's label
2025-02-21 14:22:40 -06:00

195 lines
6.9 KiB
Python

# 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:
if solid_name not in highlight_colors_for_solid:
solid_name = 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()