diff --git a/src/Gui/CommandView.cpp b/src/Gui/CommandView.cpp index 65e1502aa0..e9d0b72c21 100644 --- a/src/Gui/CommandView.cpp +++ b/src/Gui/CommandView.cpp @@ -28,6 +28,7 @@ # include # include # include +# include # include # include # include @@ -47,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -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(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(local.x()), + static_cast(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(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 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(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(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()); diff --git a/src/Gui/Inventor/So3DAnnotation.cpp b/src/Gui/Inventor/So3DAnnotation.cpp index 50ba2b10ad..a6766a3ff0 100644 --- a/src/Gui/Inventor/So3DAnnotation.cpp +++ b/src/Gui/Inventor/So3DAnnotation.cpp @@ -29,17 +29,22 @@ #include #endif #include +#include #endif #include "So3DAnnotation.h" +#include 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(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(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(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); diff --git a/src/Gui/Inventor/So3DAnnotation.h b/src/Gui/Inventor/So3DAnnotation.h index 0e2a62c42a..fee78372d4 100644 --- a/src/Gui/Inventor/So3DAnnotation.h +++ b/src/Gui/Inventor/So3DAnnotation.h @@ -27,6 +27,7 @@ #include #include #include +#include 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 paths; }; /*! @brief 3D Annotation Node - Annotation with depth buffer diff --git a/src/Gui/Navigation/NavigationStyle.cpp b/src/Gui/Navigation/NavigationStyle.cpp index 9a5761bb82..9c97445e34 100644 --- a/src/Gui/Navigation/NavigationStyle.cpp +++ b/src/Gui/Navigation/NavigationStyle.cpp @@ -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& 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() diff --git a/src/Gui/Navigation/NavigationStyle.h b/src/Gui/Navigation/NavigationStyle.h index 348bbea95c..4faf708175 100644 --- a/src/Gui/Navigation/NavigationStyle.h +++ b/src/Gui/Navigation/NavigationStyle.h @@ -36,11 +36,13 @@ #include #include +#include #include #include #include #include #include +#include // forward declarations class SoEvent; @@ -195,6 +197,8 @@ public: SbVec3f getRotationCenter(SbBool&) const; + std::optional& 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 rightClickPosition; + private: friend class NavigationAnimator; diff --git a/src/Gui/Selection/Selection.cpp b/src/Gui/Selection/Selection.cpp index baa7b87669..5bb307917b 100644 --- a/src/Gui/Selection/Selection.cpp +++ b/src/Gui/Selection/Selection.cpp @@ -2604,3 +2604,11 @@ PyObject *SelectionSingleton::sGetSelectionFromStack(PyObject * /*self*/, PyObje } PY_CATCH; } + +bool SelectionSingleton::isClarifySelectionActive() { + return clarifySelectionActive; +} + +void SelectionSingleton::setClarifySelectionActive(bool active) { + clarifySelectionActive = active; +} diff --git a/src/Gui/Selection/Selection.h b/src/Gui/Selection/Selection.h index f207474f26..70cb93f388 100644 --- a/src/Gui/Selection/Selection.h +++ b/src/Gui/Selection/Selection.h @@ -409,6 +409,9 @@ public: */ void setVisible(VisibleState visible); + bool isClarifySelectionActive(); + void setClarifySelectionActive(bool active); + /// signal on new object boost::signals2::signal signalSelectionChanged; @@ -708,6 +711,7 @@ protected: int logDisabled = 0; bool logHasSelection = false; + bool clarifySelectionActive = false; SelectionStyle selectionStyle; }; diff --git a/src/Gui/Selection/SelectionView.cpp b/src/Gui/Selection/SelectionView.cpp index c40a17fa1b..08dac85922 100644 --- a/src/Gui/Selection/SelectionView.cpp +++ b/src/Gui/Selection/SelectionView.cpp @@ -31,17 +31,22 @@ #include #include #include +#include #endif #include #include +#include #include +#include +#include #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 indices; +}; + +struct SubMenuInfo { + QMenu *menu = nullptr; + // Map from sub-object label to map from object path to element info + std::map> items; +}; + +PickData SelectionMenu::doPick(const std::vector &sels, const QPoint& pos) +{ + clear(); + Gui::Selection().setClarifySelectionActive(true); + + currentSelections = sels; + + std::map menus; + processSelections(currentSelections, menus); + buildMenuStructure(menus, currentSelections); + + QAction* picked = this->exec(pos); + return onPicked(picked, currentSelections); +} + +void SelectionMenu::processSelections(std::vector &selections, std::map &menus) +{ + std::map icons; + std::set createdElementTypes; + std::set 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 &menus, const std::vector &selections) +{ + std::vector preferredOrder = {"Object", "Solid", "Face", "Edge", "Vertex", "Wire", "Shell", "Compound", "CompSolid"}; + std::vector::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 &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 &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 &menus, std::set &createdTypes) +{ + auto geoFeature = freecad_cast(sobj->getLinkedObject(true)); + if (geoFeature) { + std::vector 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 &selections, std::map &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(sobj->getLinkedObject(true)); + if (geoFeature) { + std::vector 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(sobj->getLinkedObject(true)); + if (geoFeature) { + std::vector 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 &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 &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" diff --git a/src/Gui/Selection/SelectionView.h b/src/Gui/Selection/SelectionView.h index 318d67d6bc..f051c14ebd 100644 --- a/src/Gui/Selection/SelectionView.h +++ b/src/Gui/Selection/SelectionView.h @@ -25,6 +25,12 @@ #include "DockWindow.h" #include "Selection.h" +#include +#include +#include +#include +#include +#include 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 &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 &sels); + +private: + void processSelections(std::vector &selections, std::map &menus); + void buildMenuStructure(std::map &menus, const std::vector &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 &icons); + void addGeoFeatureTypes(App::DocumentObject* sobj, std::map &menus, std::set &createdTypes); + void addWholeObjectSelection(const PickData &sel, App::DocumentObject* sobj, std::vector &selections, + std::map &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 &selections); + void createGroupedMenu(ElementInfo &elementInfo, QMenu *parentMenu, const std::string &label, + const std::string &elementType, const std::vector &selections); + + QPointer activeMenu; + QPointer activeAction; + std::vector currentSelections; +}; + } // namespace Gui #endif // GUI_DOCKWND_SELECTIONVIEW_H diff --git a/src/Gui/Selection/SoFCUnifiedSelection.cpp b/src/Gui/Selection/SoFCUnifiedSelection.cpp index 88c6aa619d..def60c048e 100644 --- a/src/Gui/Selection/SoFCUnifiedSelection.cpp +++ b/src/Gui/Selection/SoFCUnifiedSelection.cpp @@ -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(new SoPath(2)); + pathToHighlight->ref(); + pathToHighlight->append(vp->getRoot()); + } + } + } else { + detail = vp->getDetail(subName); + pathToHighlight = static_cast(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(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(sa.getPath()->copy()); - currentHighlightPath->ref(); } if (useNewSelection.getValue()) diff --git a/src/Gui/View3DInventorViewer.cpp b/src/Gui/View3DInventorViewer.cpp index f96ac8c28a..1d9bcbbe61 100644 --- a/src/Gui/View3DInventorViewer.cpp +++ b/src/Gui/View3DInventorViewer.cpp @@ -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(event); + if (mouseEvent->button() == Qt::LeftButton) { + currentViewer = static_cast(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(event); + if (mouseEvent->button() == Qt::LeftButton) { + longPressTimer->stop(); + currentViewer = nullptr; + } + } + else if (event->type() == QEvent::MouseMove) { + if (longPressTimer->isActive()) { + auto mouseEvent = static_cast(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&) { diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index bf44bf0523..8f84d3111e 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -723,6 +723,7 @@ MenuItem* StdWorkbench::setupMenuBar() const << "Separator"; #endif *tool << "Std_Measure" + << "Std_ClarifySelection" << "Std_QuickMeasure" << "Std_UnitsCalculator" << "Separator" diff --git a/src/Mod/Import/TestImportGui.py b/src/Mod/Import/TestImportGui.py index 9f2d6eb25a..8a0cfdb707 100644 --- a/src/Mod/Import/TestImportGui.py +++ b/src/Mod/Import/TestImportGui.py @@ -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) diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp index 934ae99343..14da9ed8e9 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp @@ -44,10 +44,18 @@ # include # include # include +# include +# include #endif #include +#include +#include #include "SoBrepEdgeSet.h" +#include "SoBrepFaceSet.h" +#include "ViewProviderExt.h" + +#include using namespace PartGui; @@ -79,6 +87,26 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action) SelContextPtr ctx = Gui::SoFCSelectionRoot::getRenderContext(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()) diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.h b/src/Mod/Part/Gui/SoBrepEdgeSet.h index c63674af0d..a341c027dd 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.h +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.h @@ -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&) 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 diff --git a/src/Mod/Part/Gui/SoBrepFaceSet.cpp b/src/Mod/Part/Gui/SoBrepFaceSet.cpp index e8ada70029..8ef68c572a 100644 --- a/src/Mod/Part/Gui/SoBrepFaceSet.cpp +++ b/src/Mod/Part/Gui/SoBrepFaceSet.cpp @@ -73,8 +73,12 @@ #include #include #include +#include #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(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 diff --git a/src/Mod/Part/Gui/SoBrepFaceSet.h b/src/Mod/Part/Gui/SoBrepFaceSet.h index bab9e49414..17e33c7aed 100644 --- a/src/Mod/Part/Gui/SoBrepFaceSet.h +++ b/src/Mod/Part/Gui/SoBrepFaceSet.h @@ -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 pimpl; + + // backreference to viewprovider that owns this node + ViewProviderPartExt* viewProvider = nullptr; }; } // namespace PartGui diff --git a/src/Mod/Part/Gui/SoBrepPointSet.cpp b/src/Mod/Part/Gui/SoBrepPointSet.cpp index d674011d3f..8d8b8d6009 100644 --- a/src/Mod/Part/Gui/SoBrepPointSet.cpp +++ b/src/Mod/Part/Gui/SoBrepPointSet.cpp @@ -44,7 +44,9 @@ #endif #include +#include +#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::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) diff --git a/src/Mod/Part/Gui/SoBrepPointSet.h b/src/Mod/Part/Gui/SoBrepPointSet.h index 1dd87e8c55..d9ff7f9f2a 100644 --- a/src/Mod/Part/Gui/SoBrepPointSet.h +++ b/src/Mod/Part/Gui/SoBrepPointSet.h @@ -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 diff --git a/src/Mod/Part/Gui/ViewProviderExt.cpp b/src/Mod/Part/Gui/ViewProviderExt.cpp index 3de944e65f..c2c326b808 100644 --- a/src/Mod/Part/Gui/ViewProviderExt.cpp +++ b/src/Mod/Part/Gui/ViewProviderExt.cpp @@ -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); diff --git a/src/Mod/Part/Gui/ViewProviderExt.h b/src/Mod/Part/Gui/ViewProviderExt.h index 1f6950d50f..b5d62dfcdb 100644 --- a/src/Mod/Part/Gui/ViewProviderExt.h +++ b/src/Mod/Part/Gui/ViewProviderExt.h @@ -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; diff --git a/src/Mod/Part/parttests/ColorPerFaceTest.py b/src/Mod/Part/parttests/ColorPerFaceTest.py index 3086efa738..85cdf8a858 100644 --- a/src/Mod/Part/parttests/ColorPerFaceTest.py +++ b/src/Mod/Part/parttests/ColorPerFaceTest.py @@ -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)