Merge pull request #22029 from tetektoza/realthunder_multiselect_without_pie

Gui: Add a context menu to select obstructed items (from RT's fork Pick Geometry)
This commit is contained in:
Kacper Donat
2025-09-07 17:27:18 +02:00
committed by GitHub
22 changed files with 1092 additions and 45 deletions

View File

@@ -28,6 +28,7 @@
# include <Inventor/events/SoMouseButtonEvent.h>
# include <Inventor/nodes/SoOrthographicCamera.h>
# include <Inventor/nodes/SoPerspectiveCamera.h>
# include <Inventor/SoPickedPoint.h>
# include <QApplication>
# include <QDialog>
# include <QDomDocument>
@@ -47,6 +48,7 @@
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/DocumentObjectGroup.h>
#include <App/DocumentObserver.h>
#include <App/GeoFeature.h>
#include <App/GeoFeatureGroupExtension.h>
#include <App/Part.h>
@@ -73,6 +75,7 @@
#include "OverlayManager.h"
#include "SceneInspector.h"
#include "Selection.h"
#include "Selection/SelectionView.h"
#include "SelectionObject.h"
#include "SoFCOffscreenRenderer.h"
#include "TextureMapping.h"
@@ -3953,6 +3956,141 @@ bool StdCmdAlignToSelection::isActive()
return getGuiApplication()->sendHasMsgToActiveView("AlignToSelection");
}
//===========================================================================
// Std_ClarifySelection
//===========================================================================
DEF_STD_CMD_A(StdCmdClarifySelection)
StdCmdClarifySelection::StdCmdClarifySelection()
: Command("Std_ClarifySelection")
{
sGroup = "View";
sMenuText = QT_TR_NOOP("Clarify Selection");
sToolTipText = QT_TR_NOOP("Displays a context menu at the mouse cursor to select overlapping "
"or obstructed geometry in the 3D view.\n");
sWhatsThis = "Std_ClarifySelection";
sStatusTip = sToolTipText;
sAccel = "G, G";
eType = NoTransaction | AlterSelection;
}
void StdCmdClarifySelection::activated(int iMsg)
{
Q_UNUSED(iMsg);
// Get the active view
auto view3d = freecad_cast<View3DInventor*>(Application::Instance->activeView());
if (!view3d) {
return;
}
auto viewer = view3d->getViewer();
if (!viewer) {
return;
}
QWidget* widget = viewer->getGLWidget();
if (!widget) {
return;
}
// check if we have a stored right-click position (context menu) or should use current cursor position (keyboard shortcut)
SbVec2s point;
auto& storedPosition = viewer->navigationStyle()->getRightClickPosition();
if (storedPosition.has_value()) {
point = storedPosition.value();
} else {
QPoint pos = QCursor::pos();
QPoint local = widget->mapFromGlobal(pos);
point = SbVec2s(static_cast<short>(local.x()),
static_cast<short>(widget->height() - local.y() - 1));
}
// Use ray picking to get all objects under cursor
SoRayPickAction pickAction(viewer->getSoRenderManager()->getViewportRegion());
pickAction.setPoint(point);
constexpr double defaultMultiplier = 5.0F;
double clarifyRadiusMultiplier = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/View")
->GetFloat("ClarifySelectionRadiusMultiplier", defaultMultiplier);
pickAction.setRadius(viewer->getPickRadius() * clarifyRadiusMultiplier);
pickAction.setPickAll(static_cast<SbBool>(true)); // Get all objects under cursor
pickAction.apply(viewer->getSoRenderManager()->getSceneGraph());
const SoPickedPointList& pplist = pickAction.getPickedPointList();
if (pplist.getLength() == 0) {
return;
}
// Convert picked points to PickData list
std::vector<PickData> selections;
for (int i = 0; i < pplist.getLength(); ++i) {
SoPickedPoint* pp = pplist[i];
if (!pp || !pp->getPath()) {
continue;
}
ViewProvider* vp = viewer->getViewProviderByPath(pp->getPath());
if (!vp) {
continue;
}
// Cast to ViewProviderDocumentObject to get the object
auto vpDoc = freecad_cast<Gui::ViewProviderDocumentObject*>(vp);
if (!vpDoc) {
continue;
}
App::DocumentObject* obj = vpDoc->getObject();
if (!obj) {
continue;
}
// Get element information - handle sub-objects like Assembly parts
std::string elementName = vp->getElement(pp->getDetail());
std::string subName;
// Try to get more detailed sub-object information
bool hasSubObject = false;
if (vp->getElementPicked(pp, subName)) {
hasSubObject = true;
}
// Create PickData with selection information
PickData pickData {.obj = obj,
.element = elementName,
.docName = obj->getDocument()->getName(),
.objName = obj->getNameInDocument(),
.subName = hasSubObject ? subName : elementName};
selections.push_back(pickData);
}
if (selections.empty()) {
return;
}
QPoint globalPos;
if (storedPosition.has_value()) {
globalPos = widget->mapToGlobal(QPoint(point[0], widget->height() - point[1] - 1));
} else {
globalPos = QCursor::pos();
}
// Use SelectionMenu to display and handle the pick menu
SelectionMenu contextMenu(widget);
contextMenu.doPick(selections, globalPos);
}
bool StdCmdClarifySelection::isActive()
{
return qobject_cast<View3DInventor*>(getMainWindow()->activeWindow()) != nullptr;
}
//===========================================================================
// Instantiation
//===========================================================================
@@ -3985,6 +4123,7 @@ void CreateViewStdCommands()
rcCmdMgr.addCommand(new StdRecallWorkingView());
rcCmdMgr.addCommand(new StdCmdViewGroup());
rcCmdMgr.addCommand(new StdCmdAlignToSelection());
rcCmdMgr.addCommand(new StdCmdClarifySelection());
rcCmdMgr.addCommand(new StdCmdViewExample1());
rcCmdMgr.addCommand(new StdCmdViewExample2());

View File

@@ -29,17 +29,22 @@
#include <GL/gl.h>
#endif
#include <Inventor/elements/SoCacheElement.h>
#include <algorithm>
#endif
#include "So3DAnnotation.h"
#include <Gui/Selection/Selection.h>
using namespace Gui;
SO_ELEMENT_SOURCE(SoDelayedAnnotationsElement);
bool SoDelayedAnnotationsElement::isProcessingDelayedPaths = false;
void SoDelayedAnnotationsElement::init(SoState* state)
{
SoElement::init(state);
paths.clear();
}
void SoDelayedAnnotationsElement::initClass()
@@ -49,21 +54,64 @@ void SoDelayedAnnotationsElement::initClass()
SO_ENABLE(SoGLRenderAction, SoDelayedAnnotationsElement);
}
void SoDelayedAnnotationsElement::addDelayedPath(SoState* state, SoPath* path)
void SoDelayedAnnotationsElement::addDelayedPath(SoState* state, SoPath* path, int priority)
{
auto elt = static_cast<SoDelayedAnnotationsElement*>(state->getElementNoPush(classStackIndex));
elt->paths.append(path);
// add to unified storage with specified priority (default = 0)
elt->paths.emplace_back(path, priority);
}
SoPathList SoDelayedAnnotationsElement::getDelayedPaths(SoState* state)
{
auto elt = static_cast<SoDelayedAnnotationsElement*>(state->getElementNoPush(classStackIndex));
auto copy = elt->paths;
if (elt->paths.empty()) {
return {};
}
// sort by priority (lower numbers render first)
std::stable_sort(elt->paths.begin(), elt->paths.end(),
[](const PriorityPath& a, const PriorityPath& b) {
return a.priority < b.priority;
});
SoPathList sortedPaths;
for (const auto& priorityPath : elt->paths) {
sortedPaths.append(priorityPath.path);
}
// Clear storage
elt->paths.clear();
return sortedPaths;
}
elt->paths.truncate(0);
return copy;
void SoDelayedAnnotationsElement::processDelayedPathsWithPriority(SoState* state, SoGLRenderAction* action)
{
auto elt = static_cast<SoDelayedAnnotationsElement*>(state->getElementNoPush(classStackIndex));
if (elt->paths.empty()) {
return;
}
std::stable_sort(elt->paths.begin(), elt->paths.end(),
[](const PriorityPath& a, const PriorityPath& b) {
return a.priority < b.priority;
});
isProcessingDelayedPaths = true;
for (const auto& priorityPath : elt->paths) {
SoPathList singlePath;
singlePath.append(priorityPath.path);
action->apply(singlePath, TRUE);
}
isProcessingDelayedPaths = false;
elt->paths.clear();
}
SO_NODE_SOURCE(So3DAnnotation);

View File

@@ -27,6 +27,7 @@
#include <Inventor/elements/SoElement.h>
#include <Inventor/elements/SoSubElement.h>
#include <FCGlobal.h>
#include <vector>
namespace Gui
{
@@ -43,6 +44,15 @@ protected:
SoDelayedAnnotationsElement& operator=(const SoDelayedAnnotationsElement& other) = default;
SoDelayedAnnotationsElement& operator=(SoDelayedAnnotationsElement&& other) noexcept = default;
// internal structure to hold path with it's rendering
// priority (lower renders first)
struct PriorityPath {
SoPath* path;
int priority;
PriorityPath(SoPath* p, int pr = 0) : path(p), priority(pr) {}
};
public:
SoDelayedAnnotationsElement(const SoDelayedAnnotationsElement& other) = delete;
SoDelayedAnnotationsElement(SoDelayedAnnotationsElement&& other) noexcept = delete;
@@ -51,8 +61,13 @@ public:
static void initClass();
static void addDelayedPath(SoState* state, SoPath* path);
static void addDelayedPath(SoState* state, SoPath* path, int priority = 0);
static SoPathList getDelayedPaths(SoState* state);
static void processDelayedPathsWithPriority(SoState* state, SoGLRenderAction* action);
static bool isProcessingDelayedPaths;
SbBool matches([[maybe_unused]] const SoElement* element) const override
{
@@ -64,7 +79,8 @@ public:
return nullptr;
}
SoPathList paths;
private:
std::vector<PriorityPath> paths;
};
/*! @brief 3D Annotation Node - Annotation with depth buffer

View File

@@ -48,6 +48,8 @@
#include "Navigation/NavigationStyle.h"
#include "Navigation/NavigationStylePy.h"
#include "Application.h"
#include "Command.h"
#include "Action.h"
#include "Inventor/SoMouseWheelEvent.h"
#include "MenuManager.h"
#include "MouseSelection.h"
@@ -1022,6 +1024,11 @@ SbVec3f NavigationStyle::getRotationCenter(SbBool& found) const
return this->rotationCenter;
}
std::optional<SbVec2s>& NavigationStyle::getRightClickPosition()
{
return rightClickPosition;
}
void NavigationStyle::setRotationCenter(const SbVec3f& cnt)
{
this->rotationCenter = cnt;
@@ -1932,9 +1939,39 @@ SbBool NavigationStyle::isPopupMenuEnabled() const
return this->menuenabled;
}
bool NavigationStyle::isNavigationStyleAction(QAction* action, QActionGroup* navMenuGroup) const
{
return action && navMenuGroup->actions().indexOf(action) >= 0 && action->isChecked();
}
QWidget* NavigationStyle::findView3DInventorWidget() const
{
QWidget* widget = viewer->getWidget();
while (widget && !widget->inherits("Gui::View3DInventor")) {
widget = widget->parentWidget();
}
return widget;
}
void NavigationStyle::applyNavigationStyleChange(QAction* selectedAction)
{
QByteArray navigationStyleTypeName = selectedAction->data().toByteArray();
QWidget* view3DWidget = findView3DInventorWidget();
if (view3DWidget) {
Base::Type newNavigationStyle = Base::Type::fromName(navigationStyleTypeName.constData());
if (newNavigationStyle != this->getTypeId()) {
QEvent* navigationChangeEvent = new NavigationStyleEvent(newNavigationStyle);
QApplication::postEvent(view3DWidget, navigationChangeEvent);
}
}
}
void NavigationStyle::openPopupMenu(const SbVec2s& position)
{
Q_UNUSED(position);
// store the right-click position for potential use by Clarify Selection
rightClickPosition = position;
// ask workbenches and view provider, ...
MenuItem view;
Gui::Application::Instance->setupContextMenu("View", &view);
@@ -1953,6 +1990,7 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position)
QAction *item = navMenuGroup->addAction(name);
navMenu->addAction(item);
item->setCheckable(true);
item->setData(QByteArray(style.first.getName()));
if (const Base::Type item_style = style.first; item_style != this->getTypeId()) {
auto triggeredFun = [this, item_style](){
@@ -1970,7 +2008,58 @@ void NavigationStyle::openPopupMenu(const SbVec2s& position)
item->setChecked(true);
}
contextMenu->popup(QCursor::pos());
// Add Clarify Selection option if there are objects under cursor
bool separator = false;
auto posAction = !contextMenu->actions().empty() ? contextMenu->actions().front() : nullptr;
// Get picked objects at position
SoRayPickAction rp(viewer->getSoRenderManager()->getViewportRegion());
rp.setPoint(position);
rp.setRadius(viewer->getPickRadius());
rp.setPickAll(true);
rp.apply(viewer->getSoRenderManager()->getSceneGraph());
const SoPickedPointList& pplist = rp.getPickedPointList();
QAction *pickAction = nullptr;
if (pplist.getLength() > 0) {
separator = true;
if (auto cmd =
Application::Instance->commandManager().getCommandByName("Std_ClarifySelection")) {
pickAction = new QAction(cmd->getAction()->text(), contextMenu);
pickAction->setShortcut(cmd->getAction()->shortcut());
} else {
pickAction = new QAction(QObject::tr("Clarify Selection"), contextMenu);
}
if (posAction) {
contextMenu->insertAction(posAction, pickAction);
contextMenu->insertSeparator(posAction);
} else {
contextMenu->addAction(pickAction);
}
}
if (separator && posAction)
contextMenu->insertSeparator(posAction);
QAction* selectedAction = contextMenu->exec(QCursor::pos());
// handle navigation style change if user selected a navigation style option
if (selectedAction && isNavigationStyleAction(selectedAction, navMenuGroup)) {
applyNavigationStyleChange(selectedAction);
rightClickPosition.reset();
return;
}
if (pickAction && selectedAction == pickAction) {
// Execute the Clarify Selection command at this position
auto cmd = Application::Instance->commandManager().getCommandByName("Std_ClarifySelection");
if (cmd && cmd->isActive()) {
cmd->invoke(0); // required placeholder value - we don't use group command
}
}
rightClickPosition.reset();
}
PyObject* NavigationStyle::getPyObject()

View File

@@ -36,11 +36,13 @@
#include <Inventor/events/SoEvents.h>
#include <QEvent>
#include <QAction>
#include <Base/BaseClass.h>
#include <Base/SmartPtrPy.h>
#include <Gui/Namespace.h>
#include <FCGlobal.h>
#include <memory>
#include <optional>
// forward declarations
class SoEvent;
@@ -195,6 +197,8 @@ public:
SbVec3f getRotationCenter(SbBool&) const;
std::optional<SbVec2s>& getRightClickPosition();
PyObject *getPyObject() override;
protected:
@@ -239,6 +243,13 @@ protected:
void syncWithEvent(const SoEvent * const ev);
virtual void openPopupMenu(const SbVec2s& position);
private:
bool isNavigationStyleAction(QAction* action, QActionGroup* navMenuGroup) const;
QWidget* findView3DInventorWidget() const;
void applyNavigationStyleChange(QAction* selectedAction);
protected:
void clearLog();
void addToLog(const SbVec2s pos, const SbTime time);
@@ -290,6 +301,10 @@ protected:
Py::SmartPtr pythonObject;
// store the position where right-click occurred just before
// the menu popped up
std::optional<SbVec2s> rightClickPosition;
private:
friend class NavigationAnimator;

View File

@@ -2604,3 +2604,11 @@ PyObject *SelectionSingleton::sGetSelectionFromStack(PyObject * /*self*/, PyObje
}
PY_CATCH;
}
bool SelectionSingleton::isClarifySelectionActive() {
return clarifySelectionActive;
}
void SelectionSingleton::setClarifySelectionActive(bool active) {
clarifySelectionActive = active;
}

View File

@@ -409,6 +409,9 @@ public:
*/
void setVisible(VisibleState visible);
bool isClarifySelectionActive();
void setClarifySelectionActive(bool active);
/// signal on new object
boost::signals2::signal<void (const SelectionChanges& msg)> signalSelectionChanged;
@@ -708,6 +711,7 @@ protected:
int logDisabled = 0;
bool logHasSelection = false;
bool clarifySelectionActive = false;
SelectionStyle selectionStyle;
};

View File

@@ -31,17 +31,22 @@
#include <QTextStream>
#include <QToolButton>
#include <QVBoxLayout>
#include <set>
#endif
#include <App/ComplexGeoData.h>
#include <App/Document.h>
#include <App/ElementNamingUtils.h>
#include <App/GeoFeature.h>
#include <App/IndexedName.h>
#include <Base/Console.h>
#include "SelectionView.h"
#include "Application.h"
#include "BitmapFactory.h"
#include "Command.h"
#include "Document.h"
#include "ViewProvider.h"
FC_LOG_LEVEL_INIT("Selection", true, true, true)
@@ -704,4 +709,433 @@ void SelectionView::onEnablePickList()
/// @endcond
// SelectionMenu implementation
SelectionMenu::SelectionMenu(QWidget *parent)
: QMenu(parent)
{
connect(this, &QMenu::hovered, this, &SelectionMenu::onHover);
}
struct ElementInfo {
QMenu *menu = nullptr;
QIcon icon;
std::vector<int> indices;
};
struct SubMenuInfo {
QMenu *menu = nullptr;
// Map from sub-object label to map from object path to element info
std::map<std::string, std::map<std::string, ElementInfo>> items;
};
PickData SelectionMenu::doPick(const std::vector<PickData> &sels, const QPoint& pos)
{
clear();
Gui::Selection().setClarifySelectionActive(true);
currentSelections = sels;
std::map<std::string, SubMenuInfo> menus;
processSelections(currentSelections, menus);
buildMenuStructure(menus, currentSelections);
QAction* picked = this->exec(pos);
return onPicked(picked, currentSelections);
}
void SelectionMenu::processSelections(std::vector<PickData> &selections, std::map<std::string, SubMenuInfo> &menus)
{
std::map<App::DocumentObject*, QIcon> icons;
std::set<std::string> createdElementTypes;
std::set<std::string> processedItems;
for (int i = 0; i < (int)selections.size(); ++i) {
const auto &sel = selections[i];
App::DocumentObject* sobj = getSubObject(sel);
std::string elementType = extractElementType(sel);
std::string objKey = createObjectKey(sel);
std::string itemId = elementType + "|" + std::string(sobj->Label.getValue()) + "|" + sel.subName;
if (processedItems.find(itemId) != processedItems.end()) {
continue;
}
processedItems.insert(itemId);
QIcon icon = getOrCreateIcon(sobj, icons);
auto &elementInfo = menus[elementType].items[sobj->Label.getValue()][objKey];
elementInfo.icon = icon;
elementInfo.indices.push_back(i);
addGeoFeatureTypes(sobj, menus, createdElementTypes);
addWholeObjectSelection(sel, sobj, selections, menus, icon);
}
}
void SelectionMenu::buildMenuStructure(std::map<std::string, SubMenuInfo> &menus, const std::vector<PickData> &selections)
{
std::vector<std::string> preferredOrder = {"Object", "Solid", "Face", "Edge", "Vertex", "Wire", "Shell", "Compound", "CompSolid"};
std::vector<std::map<std::string, SubMenuInfo>::iterator> menuArray;
menuArray.reserve(menus.size());
for (const auto& category : preferredOrder) {
if (auto it = menus.find(category); it != menus.end()) {
menuArray.push_back(it);
}
}
for (auto it = menus.begin(); it != menus.end(); ++it) {
if (std::find(preferredOrder.begin(), preferredOrder.end(), it->first) == preferredOrder.end()) {
menuArray.push_back(it);
}
}
for (auto elementTypeIterator : menuArray) {
auto &elementTypeEntry = *elementTypeIterator;
auto &subMenuInfo = elementTypeEntry.second;
const std::string &elementType = elementTypeEntry.first;
if (subMenuInfo.items.empty()) {
continue;
}
subMenuInfo.menu = addMenu(QString::fromUtf8(elementType.c_str()));
// for "Object" type, and "Other", always use flat menu (no submenus for individual objects)
bool groupMenu = (elementType != "Object" && elementType != "Other") && shouldGroupMenu(subMenuInfo);
for (auto &objectLabelEntry : subMenuInfo.items) {
const std::string &objectLabel = objectLabelEntry.first;
for (auto &objectPathEntry : objectLabelEntry.second) {
auto &elementInfo = objectPathEntry.second;
if (!groupMenu) {
createFlatMenu(elementInfo, subMenuInfo.menu, objectLabel, elementType, selections);
} else {
createGroupedMenu(elementInfo, subMenuInfo.menu, objectLabel, elementType, selections);
}
}
}
}
}
PickData SelectionMenu::onPicked(QAction *picked, const std::vector<PickData> &sels)
{
// Clear the ClarifySelection active flag when menu is done
Gui::Selection().setClarifySelectionActive(false);
Gui::Selection().rmvPreselect();
if (!picked)
return PickData{};
int index = picked->data().toInt();
if (index >= 0 && index < (int)sels.size()) {
const auto &sel = sels[index];
if (sel.obj) {
Gui::Selection().addSelection(sel.docName.c_str(),
sel.objName.c_str(),
sel.subName.c_str());
}
return sel;
}
return PickData{};
}
void SelectionMenu::onHover(QAction *action)
{
if (!action || currentSelections.empty())
return;
// Clear previous preselection
Gui::Selection().rmvPreselect();
// Get the selection index from the action data
bool ok;
int index = action->data().toInt(&ok);
if (!ok || index < 0 || index >= (int)currentSelections.size())
return;
const auto &sel = currentSelections[index];
if (!sel.obj)
return;
// set preselection for both sub-objects and whole objects
Gui::Selection().setPreselect(sel.docName.c_str(),
sel.objName.c_str(),
!sel.subName.empty() ? sel.subName.c_str() : "",
0, 0, 0,
SelectionChanges::MsgSource::TreeView);
}
bool SelectionMenu::eventFilter(QObject *obj, QEvent *event)
{
return QMenu::eventFilter(obj, event);
}
void SelectionMenu::leaveEvent(QEvent *e)
{
Gui::Selection().rmvPreselect();
QMenu::leaveEvent(e);
}
App::DocumentObject* SelectionMenu::getSubObject(const PickData &sel)
{
App::DocumentObject* sobj = sel.obj;
if (!sel.subName.empty()) {
sobj = sel.obj->getSubObject(sel.subName.c_str());
if (!sobj) {
sobj = sel.obj;
}
}
return sobj;
}
std::string SelectionMenu::extractElementType(const PickData &sel)
{
std::string actualElement;
if (!sel.element.empty()) {
actualElement = sel.element;
} else if (!sel.subName.empty()) {
const char *elementName = Data::findElementName(sel.subName.c_str());
if (elementName && elementName[0]) {
actualElement = elementName;
} else {
// for link objects like "Bucket.Edge222", extract "Edge222"
std::string subName = sel.subName;
std::size_t lastDot = subName.find_last_of('.');
if (lastDot != std::string::npos && lastDot + 1 < subName.length()) {
actualElement = subName.substr(lastDot + 1);
}
}
}
if (!actualElement.empty()) {
std::size_t pos = actualElement.find_first_of("0123456789");
if (pos != std::string::npos) {
return actualElement.substr(0, pos);
}
return actualElement;
}
return "Other";
}
std::string SelectionMenu::createObjectKey(const PickData &sel)
{
std::string objKey = std::string(sel.objName);
if (!sel.subName.empty()) {
std::string subNameNoElement = sel.subName;
const char *elementName = Data::findElementName(sel.subName.c_str());
if (elementName && elementName[0]) {
std::string elementStr = elementName;
std::size_t elementPos = subNameNoElement.rfind(elementStr);
if (elementPos != std::string::npos) {
subNameNoElement = subNameNoElement.substr(0, elementPos);
}
}
objKey += "." + subNameNoElement;
}
return objKey;
}
QIcon SelectionMenu::getOrCreateIcon(App::DocumentObject* sobj, std::map<App::DocumentObject*, QIcon> &icons)
{
auto &icon = icons[sobj];
if (icon.isNull()) {
auto vp = Application::Instance->getViewProvider(sobj);
if (vp)
icon = vp->getIcon();
}
return icon;
}
void SelectionMenu::addGeoFeatureTypes(App::DocumentObject* sobj, std::map<std::string, SubMenuInfo> &menus, std::set<std::string> &createdTypes)
{
auto geoFeature = freecad_cast<App::GeoFeature*>(sobj->getLinkedObject(true));
if (geoFeature) {
std::vector<const char*> types = geoFeature->getElementTypes(true);
for (const char* type : types) {
if (type && type[0] && createdTypes.find(type) == createdTypes.end()) {
menus[type];
createdTypes.insert(type);
}
}
}
}
void SelectionMenu::addWholeObjectSelection(const PickData &sel, App::DocumentObject* sobj,
std::vector<PickData> &selections, std::map<std::string, SubMenuInfo> &menus, const QIcon &icon)
{
if (sel.subName.empty()) return;
std::string actualElement = extractElementType(sel) != "Other" ? sel.element : "";
if (actualElement.empty() && !sel.subName.empty()) {
const char *elementName = Data::findElementName(sel.subName.c_str());
if (elementName) actualElement = elementName;
}
if (actualElement.empty()) return;
bool shouldAdd = false;
if (sobj) {
if (sobj != sel.obj) {
// sub-objects
std::string typeName = sobj->getTypeId().getName();
if (typeName == "App::Part" || typeName == "PartDesign::Body") {
shouldAdd = true;
} else {
auto geoFeature = freecad_cast<App::GeoFeature*>(sobj->getLinkedObject(true));
if (geoFeature) {
std::vector<const char*> types = geoFeature->getElementTypes(true);
if (types.size() > 1) {
shouldAdd = true;
}
}
}
} else {
// top-level objects (sobj == sel.obj)
// check if subName is just an element name
if (sel.subName.find('.') == std::string::npos) {
auto geoFeature = freecad_cast<App::GeoFeature*>(sobj->getLinkedObject(true));
if (geoFeature) {
std::vector<const char*> types = geoFeature->getElementTypes(true);
if (!types.empty()) {
shouldAdd = true;
}
}
}
}
}
if (shouldAdd) {
std::string wholeObjKey;
std::string wholeObjSubName;
if (sobj != sel.obj) {
// sub-objects
std::string subNameStr = sel.subName;
std::size_t lastDot = subNameStr.find_last_of('.');
if (lastDot != std::string::npos && lastDot > 0) {
std::size_t prevDot = subNameStr.find_last_of('.', lastDot - 1);
std::string subObjName;
if (prevDot != std::string::npos) {
subObjName = subNameStr.substr(prevDot + 1, lastDot - prevDot - 1);
} else {
subObjName = subNameStr.substr(0, lastDot);
}
if (!subObjName.empty()) {
wholeObjKey = std::string(sel.objName) + "." + subObjName + ".";
wholeObjSubName = subObjName + ".";
}
}
} else {
// top-level objects (sobj == sel.obj)
wholeObjKey = std::string(sel.objName) + ".";
wholeObjSubName = ""; // empty subName for top-level whole object
}
if (!wholeObjKey.empty()) {
auto &objItems = menus["Object"].items[sobj->Label.getValue()];
if (objItems.find(wholeObjKey) == objItems.end()) {
PickData wholeObjSel = sel;
wholeObjSel.subName = wholeObjSubName;
wholeObjSel.element = "";
selections.push_back(wholeObjSel);
auto &wholeObjInfo = objItems[wholeObjKey];
wholeObjInfo.icon = icon;
wholeObjInfo.indices.push_back(selections.size() - 1);
}
}
}
}
bool SelectionMenu::shouldGroupMenu(const SubMenuInfo &info)
{
constexpr std::size_t MAX_MENU_ITEMS_BEFORE_GROUPING = 20;
if (info.items.size() > MAX_MENU_ITEMS_BEFORE_GROUPING) {
return true;
}
std::size_t objCount = 0;
std::size_t count = 0;
constexpr std::size_t MAX_SELECTION_COUNT_BEFORE_GROUPING = 5;
for (auto &objectLabelEntry : info.items) {
objCount += objectLabelEntry.second.size();
for (auto &objectPathEntry : objectLabelEntry.second)
count += objectPathEntry.second.indices.size();
if (count > MAX_SELECTION_COUNT_BEFORE_GROUPING && objCount > 1) {
return true;
}
}
return false;
}
void SelectionMenu::createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label,
const std::string &elementType, const std::vector<PickData> &selections)
{
for (int idx : elementInfo.indices) {
const auto &sel = selections[idx];
QString text = QString::fromUtf8(label.c_str());
if (!sel.element.empty()) {
text += QStringLiteral(" (%1)").arg(QString::fromUtf8(sel.element.c_str()));
} else if (!sel.subName.empty() && elementType != "Object" && elementType != "Other") {
// For link objects, extract element name from subName
// For "Bucket.Face74", we want to show "Bucket001 (Face74)"
std::string subName = sel.subName;
std::size_t lastDot = subName.find_last_of('.');
if (lastDot != std::string::npos && lastDot + 1 < subName.length()) {
QString elementName = QString::fromUtf8(subName.substr(lastDot + 1).c_str());
text += QStringLiteral(" (%1)").arg(elementName);
}
}
QAction *action = parentMenu->addAction(elementInfo.icon, text);
action->setData(idx);
connect(action, &QAction::hovered, this, [this, action]() {
onHover(action);
});
}
}
void SelectionMenu::createGroupedMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label,
const std::string &elementType, const std::vector<PickData> &selections)
{
if (!elementInfo.menu) {
elementInfo.menu = parentMenu->addMenu(elementInfo.icon, QString::fromUtf8(label.c_str()));
}
for (int idx : elementInfo.indices) {
const auto &sel = selections[idx];
QString text;
if (!sel.element.empty()) {
text = QString::fromUtf8(sel.element.c_str());
} else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') {
text = tr("Whole Object");
} else if (!sel.subName.empty()) {
// extract just the element name from subName for link objects
// for "Bucket.Edge222", we want just "Edge222"
std::string subName = sel.subName;
std::size_t lastDot = subName.find_last_of('.');
if (lastDot != std::string::npos && lastDot + 1 < subName.length()) {
text = QString::fromUtf8(subName.substr(lastDot + 1).c_str());
} else {
text = QString::fromUtf8(sel.subName.c_str());
}
} else {
text = QString::fromUtf8(sel.subName.c_str());
}
QAction *action = elementInfo.menu->addAction(text);
action->setData(idx);
connect(action, &QAction::hovered, this, [this, action]() {
onHover(action);
});
}
}
#include "moc_SelectionView.cpp"

View File

@@ -25,6 +25,12 @@
#include "DockWindow.h"
#include "Selection.h"
#include <QMenu>
#include <QPointer>
#include <map>
#include <set>
#include <string>
#include <vector>
class QListWidget;
@@ -36,6 +42,9 @@ namespace App {
class DocumentObject;
}
struct ElementInfo;
struct SubMenuInfo;
namespace Gui {
namespace DockWnd {
@@ -112,6 +121,62 @@ private:
};
} // namespace DockWnd
// Simple selection data structure
struct PickData {
App::DocumentObject* obj;
std::string element;
std::string docName;
std::string objName;
std::string subName;
};
// Add SelectionMenu class outside the DockWnd namespace
class GuiExport SelectionMenu : public QMenu {
Q_OBJECT
public:
SelectionMenu(QWidget *parent=nullptr);
/** Populate and show the menu for picking geometry elements.
*
* @param sels: a list of geometry element references
* @param pos: optional position to show the menu (defaults to current cursor position)
* @return Return the picked geometry reference
*
* The menu will be divided into submenus that are grouped by element type.
*/
PickData doPick(const std::vector<PickData> &sels, const QPoint& pos = QCursor::pos());
public Q_SLOTS:
void onHover(QAction *);
protected:
bool eventFilter(QObject *, QEvent *) override;
void leaveEvent(QEvent *e) override;
PickData onPicked(QAction *, const std::vector<PickData> &sels);
private:
void processSelections(std::vector<PickData> &selections, std::map<std::string, SubMenuInfo> &menus);
void buildMenuStructure(std::map<std::string, SubMenuInfo> &menus, const std::vector<PickData> &selections);
App::DocumentObject* getSubObject(const PickData &sel);
std::string extractElementType(const PickData &sel);
std::string createObjectKey(const PickData &sel);
QIcon getOrCreateIcon(App::DocumentObject* sobj, std::map<App::DocumentObject*, QIcon> &icons);
void addGeoFeatureTypes(App::DocumentObject* sobj, std::map<std::string, SubMenuInfo> &menus, std::set<std::string> &createdTypes);
void addWholeObjectSelection(const PickData &sel, App::DocumentObject* sobj, std::vector<PickData> &selections,
std::map<std::string, SubMenuInfo> &menus, const QIcon &icon);
bool shouldGroupMenu(const SubMenuInfo &info);
void createFlatMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label,
const std::string &elementType, const std::vector<PickData> &selections);
void createGroupedMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label,
const std::string &elementType, const std::vector<PickData> &selections);
QPointer<QMenu> activeMenu;
QPointer<QAction> activeAction;
std::vector<PickData> currentSelections;
};
} // namespace Gui
#endif // GUI_DOCKWND_SELECTIONVIEW_H

View File

@@ -367,20 +367,51 @@ void SoFCUnifiedSelection::doAction(SoAction *action)
App::Document* doc = App::GetApplication().getDocument(preselectAction->SelChange.pDocName);
App::DocumentObject* obj = doc->getObject(preselectAction->SelChange.pObjectName);
ViewProvider*vp = Application::Instance->getViewProvider(obj);
SoDetail* detail = vp->getDetail(preselectAction->SelChange.pSubName);
// use getDetailPath() like selection does, instead of just getDetail()
SoDetail* detail = nullptr;
detailPath->truncate(0);
auto subName = preselectAction->SelChange.pSubName;
SoFullPath* pathToHighlight = nullptr;
if (vp && vp->isDerivedFrom(ViewProviderDocumentObject::getClassTypeId()) &&
(useNewSelection.getValue() || vp->useNewSelectionModel()) && vp->isSelectable()) {
// get proper detail path for sub-objects (like Assembly parts)
if (!subName || !subName[0] || vp->getDetailPath(subName, detailPath, true, detail)) {
if (detailPath->getLength()) {
pathToHighlight = detailPath;
} else {
// fallback to ViewProvider root if no specific path
pathToHighlight = static_cast<SoFullPath*>(new SoPath(2));
pathToHighlight->ref();
pathToHighlight->append(vp->getRoot());
}
}
} else {
detail = vp->getDetail(subName);
pathToHighlight = static_cast<SoFullPath*>(new SoPath(2));
pathToHighlight->ref();
pathToHighlight->append(vp->getRoot());
}
SoHighlightElementAction highlightAction;
highlightAction.setHighlighted(true);
highlightAction.setColor(this->colorHighlight.getValue());
highlightAction.setElement(detail);
highlightAction.apply(vp->getRoot());
if (pathToHighlight) {
SoHighlightElementAction highlightAction;
highlightAction.setHighlighted(true);
highlightAction.setColor(this->colorHighlight.getValue());
highlightAction.setElement(detail);
highlightAction.apply(pathToHighlight);
currentHighlightPath = static_cast<SoFullPath*>(pathToHighlight->copy());
currentHighlightPath->ref();
// clean up temporary path if we created one
if (pathToHighlight != detailPath) {
pathToHighlight->unref();
}
}
delete detail;
SoSearchAction sa;
sa.setNode(vp->getRoot());
sa.apply(vp->getRoot());
currentHighlightPath = static_cast<SoFullPath*>(sa.getPath()->copy());
currentHighlightPath->ref();
}
if (useNewSelection.getValue())

View File

@@ -106,6 +106,7 @@
#include "View3DInventorViewer.h"
#include "Application.h"
#include "Command.h"
#include "Document.h"
#include "GLPainter.h"
#include "Inventor/SoAxisCrossKit.h"
@@ -231,9 +232,26 @@ while the progress bar is running.
class Gui::ViewerEventFilter : public QObject
{
public:
ViewerEventFilter() = default;
ViewerEventFilter() : longPressTimer(new QTimer(this)) {
longPressTimer->setSingleShot(true);
connect(longPressTimer, &QTimer::timeout, [this]() {
if (currentViewer) {
triggerClarifySelection();
}
});
}
~ViewerEventFilter() override = default;
private:
void triggerClarifySelection() {
Gui::Command::runCommand(Gui::Command::Gui, "Gui.runCommand('Std_ClarifySelection')");
}
QTimer* longPressTimer;
QPoint pressPosition;
View3DInventorViewer* currentViewer = nullptr;
public:
bool eventFilter(QObject* obj, QEvent* event) override {
// Bug #0000607: Some mice also support horizontal scrolling which however might
// lead to some unwanted zooming when pressing the MMB for panning.
@@ -276,6 +294,37 @@ public:
}
}
if (event->type() == QEvent::MouseButtonPress) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
currentViewer = static_cast<View3DInventorViewer*>(obj);
pressPosition = mouseEvent->pos();
int longPressTimeout = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/View")
->GetInt("LongPressTimeout", 1000);
longPressTimer->setInterval(longPressTimeout);
longPressTimer->start();
}
}
else if (event->type() == QEvent::MouseButtonRelease) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
longPressTimer->stop();
currentViewer = nullptr;
}
}
else if (event->type() == QEvent::MouseMove) {
if (longPressTimer->isActive()) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
// cancel long press if mouse moved too far (more than 5 pixels)
if ((mouseEvent->pos() - pressPosition).manhattanLength() > 5) {
longPressTimer->stop();
currentViewer = nullptr;
}
}
}
return false;
}
};
@@ -840,11 +889,10 @@ void View3DInventorViewer::onSelectionChanged(const SelectionChanges & reason)
}
if(Reason.Type == SelectionChanges::RmvPreselect ||
Reason.Type == SelectionChanges::RmvPreselectSignal)
Reason.Type == SelectionChanges::RmvPreselectSignal ||
Reason.Type == SelectionChanges::SetPreselect)
{
//Hint: do not create a tmp. instance of SelectionChanges
SelectionChanges selChanges(SelectionChanges::RmvPreselect);
SoFCPreselectionAction preselectionAction(selChanges);
SoFCPreselectionAction preselectionAction(Reason);
preselectionAction.apply(pcViewProviderRoot);
} else {
SoFCSelectionAction selectionAction(Reason);
@@ -2474,7 +2522,15 @@ void View3DInventorViewer::renderScene()
So3DAnnotation::render = true;
glClear(GL_DEPTH_BUFFER_BIT);
glra->apply(SoDelayedAnnotationsElement::getDelayedPaths(state));
// process delayed paths with priority support
if (Gui::Selection().isClarifySelectionActive()) {
Gui::SoDelayedAnnotationsElement::processDelayedPathsWithPriority(state, glra);
} else {
// standard processing for normal delayed annotations
glra->apply(Gui::SoDelayedAnnotationsElement::getDelayedPaths(state));
}
So3DAnnotation::render = false;
}
catch (const Base::MemoryException&) {

View File

@@ -723,6 +723,7 @@ MenuItem* StdWorkbench::setupMenuBar() const
<< "Separator";
#endif
*tool << "Std_Measure"
<< "Std_ClarifySelection"
<< "Std_QuickMeasure"
<< "Std_UnitsCalculator"
<< "Separator"

View File

@@ -77,7 +77,7 @@ class ExportImportTest(unittest.TestCase):
sa.apply(feature.ViewObject.RootNode)
paths = sa.getPaths()
bind = paths.get(2).getTail()
bind = paths.get(1).getTail()
self.assertEqual(bind.value.getValue(), bind.PER_PART)
sa = coin.SoSearchAction()
@@ -87,5 +87,5 @@ class ExportImportTest(unittest.TestCase):
sa.apply(feature.ViewObject.RootNode)
paths = sa.getPaths()
mat = paths.get(2).getTail()
mat = paths.get(1).getTail()
self.assertEqual(mat.diffuseColor.getNum(), 6)

View File

@@ -44,10 +44,18 @@
# include <Inventor/elements/SoLineWidthElement.h>
# include <Inventor/errors/SoDebugError.h>
# include <Inventor/misc/SoState.h>
# include <Inventor/nodes/SoGroup.h>
# include <Inventor/actions/SoSearchAction.h>
#endif
#include <Gui/Selection/SoFCUnifiedSelection.h>
#include <Gui/Selection/Selection.h>
#include <Base/Console.h>
#include "SoBrepEdgeSet.h"
#include "SoBrepFaceSet.h"
#include "ViewProviderExt.h"
#include <Gui/Inventor/So3DAnnotation.h>
using namespace PartGui;
@@ -79,6 +87,26 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action)
SelContextPtr ctx = Gui::SoFCSelectionRoot::getRenderContext<SelContext>(this,selContext,ctx2);
if(ctx2 && ctx2->selectionIndex.empty())
return;
bool hasContextHighlight = ctx && !ctx->hl.empty();
bool hasFaceHighlight = viewProvider && viewProvider->isFaceHighlightActive();
bool hasAnyHighlight = hasContextHighlight || hasFaceHighlight;
if (Gui::Selection().isClarifySelectionActive()
&& !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths
&& hasAnyHighlight) {
// if we are using clarifyselection - add this to delayed paths with priority
// as we want to get this rendered on top of everything
if (viewProvider) {
viewProvider->setFaceHighlightActive(true);
}
Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(),
action->getCurPath()->copy(),
200);
return;
}
if(selContext2->checkGlobal(ctx)) {
if(selContext2->isSelectAll()) {
selContext2->sl.clear();
@@ -132,9 +160,22 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action)
}
if(ctx2 && !ctx2->selectionIndex.empty())
renderSelection(action,ctx2,false);
else
else if (Gui::Selection().isClarifySelectionActive()
&& !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths && hasAnyHighlight) {
state->push();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthMask(false);
glDisable(GL_DEPTH_TEST);
inherited::GLRender(action);
state->pop();
}
else {
inherited::GLRender(action);
}
// Workaround for #0000433
//#if !defined(FC_OS_WIN32)
if(!action->isRenderingDelayedPaths())

View File

@@ -36,6 +36,8 @@ class SoTextureCoordinateBundle;
namespace PartGui {
class ViewProviderPartExt;
class PartGuiExport SoBrepEdgeSet : public SoIndexedLineSet {
using inherited = SoIndexedLineSet;
@@ -44,6 +46,8 @@ class PartGuiExport SoBrepEdgeSet : public SoIndexedLineSet {
public:
static void initClass();
SoBrepEdgeSet();
void setViewProvider(ViewProviderPartExt* vp) { viewProvider = vp; }
protected:
~SoBrepEdgeSet() override = default;
@@ -68,11 +72,15 @@ private:
void renderSelection(SoGLRenderAction *action, SelContextPtr, bool push=true);
bool validIndexes(const SoCoordinateElement*, const std::vector<int32_t>&) const;
private:
SelContextPtr selContext;
SelContextPtr selContext2;
Gui::SoFCSelectionCounter selCounter;
uint32_t packedColor{0};
// backreference to viewprovider that owns this node
ViewProviderPartExt* viewProvider = nullptr;
};
} // namespace PartGui

View File

@@ -73,8 +73,12 @@
#include <Gui/SoFCInteractiveElement.h>
#include <Gui/Selection/SoFCSelectionAction.h>
#include <Gui/Selection/SoFCUnifiedSelection.h>
#include <Gui/Inventor/So3DAnnotation.h>
#include "SoBrepFaceSet.h"
#include "ViewProviderExt.h"
#include "SoBrepEdgeSet.h"
using namespace PartGui;
@@ -188,6 +192,9 @@ void SoBrepFaceSet::doAction(SoAction* action)
ctx->highlightIndex = -1;
touch();
}
if (viewProvider) {
viewProvider->setFaceHighlightActive(false);
}
return;
}
@@ -204,6 +211,9 @@ void SoBrepFaceSet::doAction(SoAction* action)
ctx->highlightIndex = -1;
touch();
}
if (viewProvider) {
viewProvider->setFaceHighlightActive(false);
}
}else {
int index = static_cast<const SoFaceDetail*>(detail)->getPartIndex();
SelContextPtr ctx = Gui::SoFCSelectionRoot::getActionContext(action,this,selContext);
@@ -521,6 +531,41 @@ void SoBrepFaceSet::GLRender(SoGLRenderAction *action)
auto state = action->getState();
selCounter.checkRenderCache(state);
bool hasContextHighlight = ctx && ctx->isHighlighted() && !ctx->isHighlightAll()
&& ctx->highlightIndex >= 0 && ctx->highlightIndex < partIndex.getNum();
// for the tool add this node to delayed paths as we want to render it on top of the scene
if (Gui::Selection().isClarifySelectionActive() && hasContextHighlight) {
if (!Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) {
if (viewProvider) {
viewProvider->setFaceHighlightActive(true);
}
const SoPath* currentPath = action->getCurPath();
Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(),
currentPath->copy(),
100);
return;
} else {
// during priority delayed paths processing:
// render base faces normally first, then render highlight on top
inherited::GLRender(action);
state->push();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthMask(false);
glDisable(GL_DEPTH_TEST);
renderHighlight(action, ctx);
state->pop();
return;
}
}
// override material binding to PER_PART_INDEX to achieve
// preselection/selection with transparency
@@ -730,7 +775,7 @@ bool SoBrepFaceSet::overrideMaterialBinding(SoGLRenderAction *action, SelContext
singleColor = ctx?-1:1;
}
bool partialRender = ctx2 && !ctx2->isSelectAll();
bool partialRender = (ctx2 && !ctx2->isSelectAll());
if(singleColor>0 && !partialRender) {
//optimization for single color non-partial rendering
@@ -772,7 +817,8 @@ bool SoBrepFaceSet::overrideMaterialBinding(SoGLRenderAction *action, SelContext
packedColors.push_back(ctx->highlightColor.getPackedValue(trans0));
matIndex[ctx->highlightIndex] = packedColors.size()-1;
}
}else{
}
else{
if(partialRender) {
packedColors.push_back(SbColor(1.0,1.0,1.0).getPackedValue(1.0));
matIndex.resize(partIndex.getNum(),0);
@@ -848,7 +894,7 @@ bool SoBrepFaceSet::overrideMaterialBinding(SoGLRenderAction *action, SelContext
SoLazyElement::setPacked(state, this, packedColors.size(), packedColors.data(), hasTransparency);
SoTextureEnabledElement::set(state,this,false);
if(hasTransparency && action->isRenderingDelayedPaths()) {
if (hasTransparency && action->isRenderingDelayedPaths()) {
// rendering delayed paths means we are doing annotation (e.g.
// always on top rendering). To render transparency correctly in
// this case, we shall use openGL transparency blend. Override

View File

@@ -38,6 +38,8 @@ class SoTextureCoordinateBundle;
namespace PartGui {
class ViewProviderPartExt;
/**
* First some words to the history and the reason why we have this class:
* In older FreeCAD versions we had an own Inventor node for each sub-element of a shape with its own highlight node.
@@ -79,6 +81,8 @@ class PartGuiExport SoBrepFaceSet : public SoIndexedFaceSet {
public:
static void initClass();
SoBrepFaceSet();
void setViewProvider(ViewProviderPartExt* vp) { viewProvider = vp; }
SoMFInt32 partIndex;
@@ -154,6 +158,9 @@ private:
// Define some VBO pointer for the current mesh
class VBO;
std::unique_ptr<VBO> pimpl;
// backreference to viewprovider that owns this node
ViewProviderPartExt* viewProvider = nullptr;
};
} // namespace PartGui

View File

@@ -44,7 +44,9 @@
#endif
#include <Gui/Selection/SoFCUnifiedSelection.h>
#include <Gui/Inventor/So3DAnnotation.h>
#include "ViewProviderExt.h"
#include "SoBrepPointSet.h"
@@ -81,6 +83,22 @@ void SoBrepPointSet::GLRender(SoGLRenderAction *action)
return;
if(selContext2->checkGlobal(ctx))
ctx = selContext2;
bool hasContextHighlight =
ctx && ctx->isHighlighted() && !ctx->isHighlightAll() && ctx->highlightIndex >= 0;
// for clarifyselection, add this node to delayed path if it is highlighted and render it on
// top of everything else (highest priority)
if (Gui::Selection().isClarifySelectionActive() && hasContextHighlight
&& !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) {
if (viewProvider) {
viewProvider->setFaceHighlightActive(true);
}
Gui::SoDelayedAnnotationsElement::addDelayedPath(action->getState(),
action->getCurPath()->copy(),
300);
return;
}
if(ctx && ctx->highlightIndex == std::numeric_limits<int>::max()) {
if(ctx->selectionIndex.empty() || ctx->isSelectAll()) {
@@ -121,8 +139,15 @@ void SoBrepPointSet::GLRender(SoGLRenderAction *action)
}
if(ctx2 && !ctx2->selectionIndex.empty())
renderSelection(action,ctx2,false);
else
else if (Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) {
glPushAttrib(GL_DEPTH_BUFFER_BIT);
glDepthFunc(GL_ALWAYS);
inherited::GLRender(action);
glPopAttrib();
}
else {
inherited::GLRender(action);
}
// Workaround for #0000433
//#if !defined(FC_OS_WIN32)

View File

@@ -36,6 +36,8 @@ class SoTextureCoordinateBundle;
namespace PartGui {
class ViewProviderPartExt;
class PartGuiExport SoBrepPointSet : public SoPointSet {
using inherited = SoPointSet;
@@ -44,6 +46,8 @@ class PartGuiExport SoBrepPointSet : public SoPointSet {
public:
static void initClass();
SoBrepPointSet();
void setViewProvider(ViewProviderPartExt* vp) { viewProvider = vp; }
protected:
~SoBrepPointSet() override = default;
@@ -64,6 +68,9 @@ private:
SelContextPtr selContext2;
Gui::SoFCSelectionCounter selCounter;
uint32_t packedColor{0};
// backreference to viewprovider that owns this node
ViewProviderPartExt* viewProvider = nullptr;
};
} // namespace PartGui

View File

@@ -191,6 +191,7 @@ ViewProviderPartExt::ViewProviderPartExt()
coords = new SoCoordinate3();
coords->ref();
faceset = new SoBrepFaceSet();
faceset->setViewProvider(this);
faceset->ref();
norm = new SoNormal;
norm->ref();
@@ -198,8 +199,10 @@ ViewProviderPartExt::ViewProviderPartExt()
normb->value = SoNormalBinding::PER_VERTEX_INDEXED;
normb->ref();
lineset = new SoBrepEdgeSet();
lineset->setViewProvider(this);
lineset->ref();
nodeset = new SoBrepPointSet();
nodeset->setViewProvider(this);
nodeset->ref();
pcFaceBind = new SoMaterialBinding();
@@ -447,9 +450,9 @@ void ViewProviderPartExt::attach(App::DocumentObject *pcFeat)
// normal viewing with edges and points
pcNormalRoot->addChild(pcPointsRoot);
pcNormalRoot->addChild(wireframe);
pcNormalRoot->addChild(offset);
pcNormalRoot->addChild(pcFlatRoot);
pcNormalRoot->addChild(wireframe);
// just faces with no edges or points
pcFlatRoot->addChild(pShapeHints);

View File

@@ -156,6 +156,9 @@ public:
bool allowOverride(const App::DocumentObject &) const override;
void setFaceHighlightActive(bool active) { faceHighlightActive = active; }
bool isFaceHighlightActive() const { return faceHighlightActive; }
/** @name Edit methods */
//@{
void setupContextMenu(QMenu*, QObject*, const char*) override;
@@ -213,6 +216,7 @@ protected:
bool VisualTouched;
bool NormalsFromUV;
bool faceHighlightActive = false;
private:
Gui::ViewProviderFaceTexture texture;

View File

@@ -49,7 +49,7 @@ class ColorPerFaceTest(unittest.TestCase):
sa.apply(box.ViewObject.RootNode)
paths = sa.getPaths()
mat = paths.get(2).getTail()
mat = paths.get(1).getTail()
self.assertEqual(mat.diffuseColor.getNum(), 6)
def testBoxAndLink(self):
@@ -83,7 +83,7 @@ class ColorPerFaceTest(unittest.TestCase):
sa.apply(box.ViewObject.RootNode)
paths = sa.getPaths()
mat = paths.get(2).getTail()
mat = paths.get(1).getTail()
self.assertEqual(mat.diffuseColor.getNum(), 6)
def testTransparency(self):
@@ -110,7 +110,7 @@ class ColorPerFaceTest(unittest.TestCase):
sa.apply(box.ViewObject.RootNode)
paths = sa.getPaths()
bind = paths.get(2).getTail()
bind = paths.get(1).getTail()
self.assertEqual(bind.value.getValue(), bind.PER_PART)
sa = coin.SoSearchAction()
@@ -120,7 +120,7 @@ class ColorPerFaceTest(unittest.TestCase):
sa.apply(box.ViewObject.RootNode)
paths = sa.getPaths()
mat = paths.get(2).getTail()
mat = paths.get(1).getTail()
self.assertEqual(mat.diffuseColor.getNum(), 6)
def testMultiFuse(self):
@@ -146,7 +146,7 @@ class ColorPerFaceTest(unittest.TestCase):
sa.apply(fuse.ViewObject.RootNode)
paths = sa.getPaths()
bind = paths.get(2).getTail()
bind = paths.get(1).getTail()
self.assertEqual(bind.value.getValue(), bind.PER_PART)
sa = coin.SoSearchAction()
@@ -156,7 +156,7 @@ class ColorPerFaceTest(unittest.TestCase):
sa.apply(fuse.ViewObject.RootNode)
paths = sa.getPaths()
mat = paths.get(2).getTail()
mat = paths.get(1).getTail()
self.assertEqual(mat.diffuseColor.getNum(), 11)
self.assertEqual(len(fuse.Shape.Faces), 11)
@@ -195,7 +195,7 @@ class ColorPerFaceTest(unittest.TestCase):
sa.apply(fuse.ViewObject.RootNode)
paths = sa.getPaths()
bind = paths.get(2).getTail()
bind = paths.get(1).getTail()
self.assertEqual(bind.value.getValue(), bind.PER_PART)
sa = coin.SoSearchAction()
@@ -205,5 +205,5 @@ class ColorPerFaceTest(unittest.TestCase):
sa.apply(fuse.ViewObject.RootNode)
paths = sa.getPaths()
mat = paths.get(2).getTail()
mat = paths.get(1).getTail()
self.assertEqual(mat.diffuseColor.getNum(), 11)