Core/Gui: Render primitives on top of the scene in PickGeometry
+ added some better grouping for items, which are assigned per object right now. For example, if we exceed 10 items per object it gets an additional group. Co-authored-by: realthunder <realthunder@users.noreply.github.com>
This commit is contained in:
@@ -4036,7 +4036,15 @@ void StdCmdPickGeometry::activated(int iMsg)
|
||||
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;
|
||||
@@ -4044,7 +4052,7 @@ void StdCmdPickGeometry::activated(int iMsg)
|
||||
pickData.element = elementName;
|
||||
pickData.docName = obj->getDocument()->getName();
|
||||
pickData.objName = obj->getNameInDocument();
|
||||
pickData.subName = elementName;
|
||||
pickData.subName = hasSubObject ? subName : elementName;
|
||||
|
||||
selections.push_back(pickData);
|
||||
}
|
||||
|
||||
@@ -29,17 +29,23 @@
|
||||
#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);
|
||||
priorityPaths.clear();
|
||||
paths.truncate(0);
|
||||
}
|
||||
|
||||
void SoDelayedAnnotationsElement::initClass()
|
||||
@@ -49,21 +55,72 @@ 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));
|
||||
|
||||
// add to priority-aware storage if priority has been specified
|
||||
if (priority > 0) {
|
||||
elt->priorityPaths.emplace_back(path, priority);
|
||||
return;
|
||||
}
|
||||
|
||||
elt->paths.append(path);
|
||||
}
|
||||
|
||||
SoPathList SoDelayedAnnotationsElement::getDelayedPaths(SoState* state)
|
||||
{
|
||||
auto elt = static_cast<SoDelayedAnnotationsElement*>(state->getElementNoPush(classStackIndex));
|
||||
auto copy = elt->paths;
|
||||
|
||||
|
||||
// if we don't have priority paths, just return normal delayed paths
|
||||
if (elt->priorityPaths.empty()) {
|
||||
auto copy = elt->paths;
|
||||
elt->paths.truncate(0);
|
||||
return copy;
|
||||
}
|
||||
|
||||
// sort by priority (lower numbers render first)
|
||||
std::stable_sort(elt->priorityPaths.begin(), elt->priorityPaths.end(),
|
||||
[](const PriorityPath& a, const PriorityPath& b) {
|
||||
return a.priority < b.priority;
|
||||
});
|
||||
|
||||
SoPathList sortedPaths;
|
||||
for (const auto& priorityPath : elt->priorityPaths) {
|
||||
sortedPaths.append(priorityPath.path);
|
||||
}
|
||||
|
||||
// Clear storage
|
||||
elt->priorityPaths.clear();
|
||||
elt->paths.truncate(0);
|
||||
|
||||
return sortedPaths;
|
||||
}
|
||||
|
||||
return copy;
|
||||
void SoDelayedAnnotationsElement::processDelayedPathsWithPriority(SoState* state, SoGLRenderAction* action)
|
||||
{
|
||||
auto elt = static_cast<SoDelayedAnnotationsElement*>(state->getElementNoPush(classStackIndex));
|
||||
|
||||
if (elt->priorityPaths.empty()) return;
|
||||
|
||||
std::stable_sort(elt->priorityPaths.begin(), elt->priorityPaths.end(),
|
||||
[](const PriorityPath& a, const PriorityPath& b) {
|
||||
return a.priority < b.priority;
|
||||
});
|
||||
|
||||
isProcessingDelayedPaths = true;
|
||||
|
||||
for (const auto& priorityPath : elt->priorityPaths) {
|
||||
SoPathList singlePath;
|
||||
singlePath.append(priorityPath.path);
|
||||
|
||||
action->apply(singlePath, TRUE);
|
||||
}
|
||||
|
||||
isProcessingDelayedPaths = false;
|
||||
|
||||
elt->priorityPaths.clear();
|
||||
elt->paths.truncate(0);
|
||||
}
|
||||
|
||||
SO_NODE_SOURCE(So3DAnnotation);
|
||||
|
||||
@@ -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,6 +79,10 @@ public:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
// priority-aware paths
|
||||
std::vector<PriorityPath> priorityPaths;
|
||||
|
||||
SoPathList paths;
|
||||
};
|
||||
|
||||
|
||||
@@ -2604,3 +2604,13 @@ PyObject *SelectionSingleton::sGetSelectionFromStack(PyObject * /*self*/, PyObje
|
||||
}
|
||||
PY_CATCH;
|
||||
}
|
||||
|
||||
bool SelectionSingleton::pickGeometryActive = false;
|
||||
|
||||
bool SelectionSingleton::isPickGeometryActive() {
|
||||
return pickGeometryActive;
|
||||
}
|
||||
|
||||
void SelectionSingleton::setPickGeometryActive(bool active) {
|
||||
pickGeometryActive = active;
|
||||
}
|
||||
|
||||
@@ -291,6 +291,8 @@ private:
|
||||
class GuiExport SelectionSingleton : public Base::Subject<const SelectionChanges&>
|
||||
{
|
||||
public:
|
||||
static bool pickGeometryActive;
|
||||
|
||||
struct SelObj {
|
||||
const char* DocName;
|
||||
const char* FeatName;
|
||||
@@ -409,6 +411,9 @@ public:
|
||||
*/
|
||||
void setVisible(VisibleState visible);
|
||||
|
||||
static bool isPickGeometryActive();
|
||||
static void setPickGeometryActive(bool active);
|
||||
|
||||
/// signal on new object
|
||||
boost::signals2::signal<void (const SelectionChanges& msg)> signalSelectionChanged;
|
||||
|
||||
|
||||
@@ -31,11 +31,15 @@
|
||||
#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"
|
||||
@@ -727,75 +731,100 @@ struct SubMenuInfo {
|
||||
PickData SelectionMenu::doPick(const std::vector<PickData> &sels)
|
||||
{
|
||||
clear();
|
||||
Gui::Selection().setPickGeometryActive(true);
|
||||
|
||||
// store reference to selections for use in onHover
|
||||
currentSelections = &sels;
|
||||
std::vector<PickData> selsCopy = sels;
|
||||
currentSelections = &selsCopy;
|
||||
|
||||
std::map<std::string, std::vector<int>> typeGroups;
|
||||
|
||||
// Group selections by element type
|
||||
for (int i = 0; i < (int)sels.size(); ++i) {
|
||||
const auto &sel = sels[i];
|
||||
std::string elementType = "Other";
|
||||
|
||||
// Extract element type from element name
|
||||
if (!sel.element.empty()) {
|
||||
if (sel.element.find("Face") == 0) elementType = "Face";
|
||||
else if (sel.element.find("Edge") == 0) elementType = "Edge";
|
||||
else if (sel.element.find("Vertex") == 0) elementType = "Vertex";
|
||||
else if (sel.element.find("Wire") == 0) elementType = "Wire";
|
||||
else if (sel.element.find("Shell") == 0) elementType = "Shell";
|
||||
else if (sel.element.find("Solid") == 0) elementType = "Solid";
|
||||
}
|
||||
|
||||
typeGroups[elementType].push_back(i);
|
||||
}
|
||||
|
||||
// Create menu structure
|
||||
for (const auto &typeGroup : typeGroups) {
|
||||
const std::string &typeName = typeGroup.first;
|
||||
const std::vector<int> &indices = typeGroup.second;
|
||||
|
||||
if (indices.empty()) continue;
|
||||
|
||||
QMenu *typeMenu = nullptr;
|
||||
if (typeGroups.size() > 1) {
|
||||
typeMenu = addMenu(QString::fromUtf8(typeName.c_str()));
|
||||
}
|
||||
|
||||
for (int idx : indices) {
|
||||
const auto &sel = sels[idx];
|
||||
|
||||
QString text = QString::fromUtf8(sel.obj->Label.getValue());
|
||||
if (!sel.element.empty()) {
|
||||
text += QString::fromLatin1(" (") + QString::fromUtf8(sel.element.c_str()) + QString::fromLatin1(")");
|
||||
}
|
||||
|
||||
// Get icon from view provider
|
||||
QIcon icon;
|
||||
auto vp = Application::Instance->getViewProvider(sel.obj);
|
||||
if (vp) {
|
||||
icon = vp->getIcon();
|
||||
}
|
||||
|
||||
QAction *action;
|
||||
if (typeMenu) {
|
||||
action = typeMenu->addAction(icon, text);
|
||||
// Connect submenu hovered signals as well
|
||||
connect(typeMenu, &QMenu::hovered, this, &SelectionMenu::onHover);
|
||||
} else {
|
||||
action = addAction(icon, text);
|
||||
}
|
||||
action->setData(idx);
|
||||
}
|
||||
}
|
||||
std::map<std::string, SubMenuInfo> menus;
|
||||
processSelections(selsCopy, menus);
|
||||
buildMenuStructure(menus, selsCopy);
|
||||
|
||||
QAction* picked = this->exec(QCursor::pos());
|
||||
return onPicked(picked, sels);
|
||||
return onPicked(picked, selsCopy);
|
||||
}
|
||||
|
||||
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) {
|
||||
auto it = menus.find(category);
|
||||
if (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 it : menuArray) {
|
||||
auto &v = *it;
|
||||
auto &info = v.second;
|
||||
|
||||
if (info.items.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
info.menu = addMenu(QString::fromUtf8(v.first.c_str()));
|
||||
bool groupMenu = shouldGroupMenu(info);
|
||||
|
||||
for (auto &vv : info.items) {
|
||||
const std::string &label = vv.first;
|
||||
|
||||
for (auto &vvv : vv.second) {
|
||||
auto &elementInfo = vvv.second;
|
||||
|
||||
if (!groupMenu) {
|
||||
createFlatMenu(elementInfo, info.menu, label, v.first, selections);
|
||||
} else {
|
||||
createGroupedMenu(elementInfo, info.menu, label, v.first, selections);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PickData SelectionMenu::onPicked(QAction *picked, const std::vector<PickData> &sels)
|
||||
{
|
||||
// Clear the PickGeometry active flag when menu is done
|
||||
Gui::Selection().setPickGeometryActive(false);
|
||||
|
||||
Gui::Selection().rmvPreselect();
|
||||
if (!picked)
|
||||
return PickData{};
|
||||
@@ -831,13 +860,14 @@ void SelectionMenu::onHover(QAction *action)
|
||||
if (!sel.obj)
|
||||
return;
|
||||
|
||||
// extract just the element name (e.g., "Face1") from subName for preselection
|
||||
std::string elementName = sel.element;
|
||||
if (!elementName.empty()) {
|
||||
// use TreeView as message source for menu hover
|
||||
// For hover preselection, use the whole object path like solids do
|
||||
// This provides consistent behavior and better highlighting for assembly elements
|
||||
if (!sel.subName.empty()) {
|
||||
// Always use the full original path for consistent behavior
|
||||
// This matches how solids behave and provides better highlighting
|
||||
Gui::Selection().setPreselect(sel.docName.c_str(),
|
||||
sel.objName.c_str(),
|
||||
elementName.c_str(),
|
||||
sel.subName.c_str(),
|
||||
0, 0, 0,
|
||||
SelectionChanges::MsgSource::TreeView);
|
||||
}
|
||||
@@ -854,4 +884,205 @@ void SelectionMenu::leaveEvent(QEvent *e)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 = dynamic_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 && sobj != sel.obj) {
|
||||
std::string typeName = sobj->getTypeId().getName();
|
||||
if (typeName == "App::Part" || typeName == "PartDesign::Body") {
|
||||
shouldAdd = true;
|
||||
} else {
|
||||
auto geoFeature = dynamic_cast<App::GeoFeature*>(sobj->getLinkedObject(true));
|
||||
if (geoFeature) {
|
||||
std::vector<const char*> types = geoFeature->getElementTypes(true);
|
||||
if (types.size() > 1) {
|
||||
shouldAdd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAdd) {
|
||||
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()) {
|
||||
std::string wholeObjKey = std::string(sel.objName) + "." + subObjName + ".";
|
||||
auto &objItems = menus["Object"].items[sobj->Label.getValue()];
|
||||
if (objItems.find(wholeObjKey) == objItems.end()) {
|
||||
PickData wholeObjSel = sel;
|
||||
wholeObjSel.subName = subObjName + ".";
|
||||
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)
|
||||
{
|
||||
if (info.items.size() > 20) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t objCount = 0;
|
||||
std::size_t count = 0;
|
||||
for (auto &vv : info.items) {
|
||||
objCount += vv.second.size();
|
||||
for (auto &vvv : vv.second)
|
||||
count += vvv.second.indices.size();
|
||||
if (count > 5 && 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 += QString::fromLatin1(" (") + QString::fromUtf8(sel.element.c_str()) + QString::fromLatin1(")");
|
||||
} else if (elementType == "Object" && !sel.subName.empty() && sel.subName.back() == '.') {
|
||||
text += QString::fromLatin1(" (Whole Object)");
|
||||
}
|
||||
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 = QString::fromLatin1("Whole Object");
|
||||
} 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"
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
#include "Selection.h"
|
||||
#include <QMenu>
|
||||
#include <QPointer>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
class QListWidget;
|
||||
@@ -38,6 +42,9 @@ namespace App {
|
||||
class DocumentObject;
|
||||
}
|
||||
|
||||
struct ElementInfo;
|
||||
struct SubMenuInfo;
|
||||
|
||||
namespace Gui {
|
||||
namespace DockWnd {
|
||||
|
||||
@@ -148,6 +155,22 @@ protected:
|
||||
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;
|
||||
const std::vector<PickData>* currentSelections;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -2473,7 +2473,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().isPickGeometryActive()) {
|
||||
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&) {
|
||||
|
||||
@@ -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,20 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action)
|
||||
SelContextPtr ctx = Gui::SoFCSelectionRoot::getRenderContext<SelContext>(this,selContext,ctx2);
|
||||
if(ctx2 && ctx2->selectionIndex.empty())
|
||||
return;
|
||||
|
||||
|
||||
if (Gui::Selection()
|
||||
.isPickGeometryActive() && !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths
|
||||
&& ((ctx && !ctx->hl.empty()) || viewProvider->isFaceHighlightActive())) {
|
||||
// if we are using pickgeometry - add this to delayed paths with priority
|
||||
// as we want to get this rendered on top of everything
|
||||
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 +154,23 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction *action)
|
||||
}
|
||||
if(ctx2 && !ctx2->selectionIndex.empty())
|
||||
renderSelection(action,ctx2,false);
|
||||
else
|
||||
else if (Gui::Selection().isPickGeometryActive()
|
||||
&& ((ctx && !ctx->hl.empty()) || viewProvider->isFaceHighlightActive())
|
||||
&& Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) {
|
||||
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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,40 @@ void SoBrepFaceSet::GLRender(SoGLRenderAction *action)
|
||||
|
||||
auto state = action->getState();
|
||||
selCounter.checkRenderCache(state);
|
||||
|
||||
// for the tool add this node to delayed paths as we want to render it on top of the scene
|
||||
if (Gui::Selection().isPickGeometryActive() && ctx && ctx->isHighlighted()
|
||||
&& !ctx->isHighlightAll() && ctx->highlightIndex >= 0
|
||||
&& ctx->highlightIndex < partIndex.getNum()) {
|
||||
|
||||
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 +774,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 +816,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 +893,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
#endif
|
||||
|
||||
#include <Gui/Selection/SoFCUnifiedSelection.h>
|
||||
#include <Gui/Inventor/So3DAnnotation.h>
|
||||
|
||||
#include "ViewProviderExt.h"
|
||||
#include "SoBrepPointSet.h"
|
||||
|
||||
|
||||
@@ -81,6 +83,18 @@ void SoBrepPointSet::GLRender(SoGLRenderAction *action)
|
||||
return;
|
||||
if(selContext2->checkGlobal(ctx))
|
||||
ctx = selContext2;
|
||||
|
||||
// for pickgeometry, add this node to delayed path if it is highlighted and render it on
|
||||
// top of everything else (highest priority)
|
||||
if (Gui::Selection().isPickGeometryActive() && ctx && ctx->isHighlighted()
|
||||
&& !ctx->isHighlightAll() && ctx->highlightIndex >= 0
|
||||
&& !Gui::SoDelayedAnnotationsElement::isProcessingDelayedPaths) {
|
||||
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 +135,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user