Merge pull request #24951 from PaddleStroke/asm_jointhighlight

Assembly: Highlight joint elements on joint selection
This commit is contained in:
Kacper Donat
2026-01-22 20:16:56 +01:00
committed by GitHub
3 changed files with 259 additions and 43 deletions

View File

@@ -43,6 +43,8 @@
#include <set>
#include <algorithm>
#include <iterator>
#include <Inventor/SoPath.h>
#include <Inventor/details/SoDetail.h>
#include <App/Link.h>
#include <App/Document.h>
@@ -64,6 +66,7 @@
#include <Gui/ViewProviderLink.h>
#include <Gui/ViewProviderGeometryObject.h>
#include <Gui/ViewParams.h>
#include <Gui/Selection/SoFCSelectionAction.h>
#include <Mod/Assembly/App/AssemblyLink.h>
#include <Mod/Assembly/App/AssemblyObject.h>
@@ -1403,17 +1406,13 @@ void ViewProviderAssembly::applyIsolationRecursively(
state.visibility = current->Visibility.getValue();
if (vpl) {
state.selectable = vpl->Selectable.getValue();
state.overrideMaterial = vpl->OverrideMaterial.getValue();
state.shapeMaterial = vpl->ShapeMaterial.getValue();
}
else { // vpg
else {
state.selectable = vpg->Selectable.getValue();
state.shapeMaterial = vpg->ShapeAppearance.getValue()[0];
}
stateBackup[current] = state;
if (mode == IsolateMode::Hidden) {
stateBackup[current] = state;
current->Visibility.setValue(isolate);
return;
}
@@ -1422,39 +1421,23 @@ void ViewProviderAssembly::applyIsolationRecursively(
current->Visibility.setValue(true);
}
App::Material mat = App::Material::getDefaultAppearance();
float trans = mode == IsolateMode::Transparent ? 0.8 : 1.0;
mat.transparency = trans;
if (vpl) {
vpl->Selectable.setValue(isolate);
if (!isolate) {
vpl->OverrideMaterial.setValue(true);
vpl->ShapeMaterial.setValue(mat);
}
}
else if (vpg) {
vpg->Selectable.setValue(isolate);
if (!isolate) {
vpg->ShapeAppearance.setValue(mat);
}
// Note, this geometric object could have a link linking to it in the assembly
// and this link may be in isolate set! If so it will inherit the isolation
// from 'current'! So we need to manually handle it to visible.
const std::vector<App::DocumentObject*> inList = current->getInList();
for (auto* child : inList) {
if (child->isDerivedFrom<App::Link>() && child->getLinkedObject() == current) {
// In this case we need to reverse isolate this!
auto* childVp = freecad_cast<Gui::ViewProviderLink*>(
Gui::Application::Instance->getViewProvider(child)
);
if (!isolate) {
float trans = mode == IsolateMode::Transparent ? 0.8 : 1.0;
Base::Color transparentColor(App::Material::getDefaultAppearance().diffuseColor);
transparentColor.setTransparency(trans);
std::map<std::string, Base::Color> colorMap;
colorMap["Face"] = transparentColor; // The "Face" wildcard targets all faces
// we give the child the color the current had before we changed it
childVp->OverrideMaterial.setValue(true);
childVp->ShapeMaterial.setValue(state.shapeMaterial);
}
}
}
Gui::SoSelectionElementAction action(Gui::SoSelectionElementAction::Color, true);
action.swapColors(colorMap);
action.apply(vp->getRoot());
}
}
@@ -1497,6 +1480,8 @@ void ViewProviderAssembly::isolateJointReferences(App::DocumentObject* joint, Is
std::set<App::DocumentObject*> isolateSet = {part1, part2};
isolateComponents(isolateSet, mode);
highlightJointElements(joint);
}
void ViewProviderAssembly::clearIsolate()
@@ -1504,6 +1489,8 @@ void ViewProviderAssembly::clearIsolate()
if (isolatedJoint) {
isolatedJoint->Visibility.setValue(isolatedJointVisibilityBackup);
isolatedJoint = nullptr;
clearJointElementHighlight();
}
for (const auto& pair : stateBackup) {
@@ -1514,25 +1501,75 @@ void ViewProviderAssembly::clearIsolate()
}
component->Visibility.setValue(state.visibility);
if (auto* vpl = dynamic_cast<Gui::ViewProviderLink*>(
Gui::Application::Instance->getViewProvider(component)
)) {
auto* vp = Gui::Application::Instance->getViewProvider(component);
if (auto* vpl = dynamic_cast<Gui::ViewProviderLink*>(vp)) {
vpl->Selectable.setValue(state.selectable);
vpl->ShapeMaterial.setValue(state.shapeMaterial);
vpl->OverrideMaterial.setValue(state.overrideMaterial);
}
else if (auto* vpg = dynamic_cast<Gui::ViewProviderGeometryObject*>(
Gui::Application::Instance->getViewProvider(component)
)) {
else if (auto* vpg = dynamic_cast<Gui::ViewProviderGeometryObject*>(vp)) {
vpg->Selectable.setValue(state.selectable);
vpg->ShapeAppearance.setValue(state.shapeMaterial);
}
Gui::SoSelectionElementAction action(Gui::SoSelectionElementAction::Color, true);
action.apply(vp->getRoot());
}
stateBackup.clear();
}
void ViewProviderAssembly::highlightJointElements(App::DocumentObject* joint)
{
clearJointElementHighlight();
SbColor defaultHighlightColor(0.8f, 0.1f, 0.1f);
uint32_t defaultPacked = defaultHighlightColor.getPackedValue();
ParameterGrp::handle hGrp = Gui::WindowParameter::getDefaultParameter()->GetGroup("View");
uint32_t packedColor = hGrp->GetUnsigned("HighlightColor", defaultPacked);
Base::Color highlightColor(packedColor);
std::set<std::string> processedElements;
const char* refNames[] = {"Reference1", "Reference2"};
for (const char* refName : refNames) {
auto* propRef = dynamic_cast<App::PropertyXLinkSub*>(joint->getPropertyByName(refName));
if (!propRef || propRef->getSubValues().empty()) {
continue;
}
const auto& el = propRef->getSubValues()[0];
if (el.empty() || processedElements.count(el)) {
continue;
}
processedElements.insert(el); // Mark as processed.
auto* path = static_cast<SoFullPath*>(new SoPath(20));
SoDetail* detail = nullptr;
if (this->getDetailPath(el.c_str(), path, true, detail)) {
const char* elementNameCStr = Data::findElementName(el.c_str());
if (!elementNameCStr || !elementNameCStr[0]) {
continue;
}
std::string elementName(elementNameCStr);
Gui::SoSelectionElementAction action(Gui::SoSelectionElementAction::Color, true);
std::map<std::string, Base::Color> colorMap;
colorMap[elementName] = highlightColor;
action.swapColors(colorMap);
path->ref();
action.apply(path);
}
delete detail;
}
}
void ViewProviderAssembly::clearJointElementHighlight()
{
Gui::SoSelectionElementAction action(Gui::SoSelectionElementAction::Color, true);
// An empty color map tells nodes to clear their secondary color.
action.apply(this->getRoot());
}
void ViewProviderAssembly::slotAboutToOpenTransaction(const std::string& cmdName)
{
Q_UNUSED(cmdName);

View File

@@ -277,6 +277,9 @@ private:
App::DocumentObject* isolatedJoint {nullptr};
bool isolatedJointVisibilityBackup {false};
void highlightJointElements(App::DocumentObject* joint);
void clearJointElementHighlight();
void applyIsolationRecursively(
App::DocumentObject* current,
std::set<App::DocumentObject*>& isolateSet,

View File

@@ -62,7 +62,7 @@ using namespace PartGui;
SO_NODE_SOURCE(SoBrepEdgeSet)
struct SoBrepEdgeSet::SelContext: Gui::SoFCSelectionContext
struct SoBrepEdgeSet::SelContext: Gui::SoFCSelectionContextEx
{
std::vector<int32_t> hl, sl;
};
@@ -86,7 +86,7 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction* action)
SelContextPtr ctx2;
SelContextPtr ctx = Gui::SoFCSelectionRoot::getRenderContext<SelContext>(this, selContext, ctx2);
if (ctx2 && ctx2->selectionIndex.empty()) {
if (ctx2 && ctx2->selectionIndex.empty() && ctx2->colors.empty()) {
return;
}
@@ -128,6 +128,147 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction* action)
ctx = selContext2;
}
bool hasColorOverride = (ctx2 && !ctx2->colors.empty());
if (hasColorOverride) {
// Special handling for edge color overrides (e.g. highlighting specific edges).
// We initially attempted to use the same logic as SoBrepFaceSet (setting
// SoMaterialBindingElement::PER_PART_INDEXED and populating SoLazyElement arrays).
// However, this proved brittle for SoIndexedLineSet, causing persistent crashes
// in SoMaterialBundle/SoGLLazyElement (SIGSEGV) due to internal Coin3D state
// mismatches when mixing Lit (default) and Unlit (highlighted) states.
//
// To ensure stability, we bypass the base class GLRender entirely and perform
// a manual dual-pass render using direct OpenGL calls:
// Pass 1: Render default lines using the current Coin3D state (Lighting enabled).
// Pass 2: Render highlighted lines with Lighting disabled to ensure bright, flat colors.
state->push();
const SoCoordinateElement* coords;
const SbVec3f* normals;
const int32_t* cindices;
const int32_t* nindices;
const int32_t* tindices;
const int32_t* mindices;
int numcindices;
SbBool normalCacheUsed;
// We request normals (true) because default lines need them for lighting
this->getVertexData(
state,
coords,
normals,
cindices,
nindices,
tindices,
mindices,
numcindices,
true,
normalCacheUsed
);
const SbVec3f* coords3d = coords->getArrayPtr3();
// Apply the default material settings (Standard Lighting/Material)
// This ensures default lines look correct (e.g. Black)
SoMaterialBundle mb(action);
mb.sendFirst();
// We will collect highlighted segments to render them in a second pass
// so we don't have to switch GL state constantly.
struct HighlightSegment
{
int startIndex;
Base::Color color;
};
std::vector<HighlightSegment> highlights;
int linecount = 0;
int i = 0;
// --- PASS 1: Render Default Lines (Lit) ---
while (i < numcindices) {
int startIndex = i;
// Check if this line index has an override color
const Base::Color* pColor = nullptr;
auto it = ctx2->colors.find(linecount);
if (it != ctx2->colors.end()) {
pColor = &it->second;
}
else {
// Check for wildcard color
auto it_all = ctx2->colors.find(-1);
if (it_all != ctx2->colors.end()) {
pColor = &it_all->second;
}
}
if (pColor) {
// This is a highlighted line. Save it for Pass 2.
highlights.push_back({startIndex, *pColor});
// Skip over the indices for this line
while (i < numcindices && cindices[i] >= 0) {
i++;
}
i++; // skip the -1 separator
}
else {
// This is a default line. Render immediately with current (Lit) state.
glBegin(GL_LINE_STRIP);
while (i < numcindices) {
int32_t idx = cindices[i++];
if (idx < 0) {
break;
}
if (idx < coords->getNum()) {
if (normals) {
glNormal3fv((const GLfloat*)(normals + idx));
}
glVertex3fv((const GLfloat*)(coords3d + idx));
}
}
glEnd();
}
linecount++;
}
// --- PASS 2: Render Highlighted Lines (Unlit) ---
if (!highlights.empty()) {
// Disable lighting and textures so the color is flat and bright
glPushAttrib(GL_LIGHTING_BIT | GL_CURRENT_BIT | GL_ENABLE_BIT);
glDisable(GL_LIGHTING);
glDisable(GL_TEXTURE_2D);
for (const auto& segment : highlights) {
// Apply the explicit color from the map
// Note: FreeCAD Base::Color transparency is 0.0 (opaque) to 1.0 (transparent)
// OpenGL Alpha is 1.0 (opaque) to 0.0 (transparent)
glColor4f(segment.color.r, segment.color.g, segment.color.b, 1.0f - segment.color.a);
glBegin(GL_LINE_STRIP);
int j = segment.startIndex;
while (j < numcindices) {
int32_t idx = cindices[j++];
if (idx < 0) {
break;
}
if (idx < coords->getNum()) {
glVertex3fv((const GLfloat*)(coords3d + idx));
}
}
glEnd();
}
glPopAttrib();
}
// Do NOT call inherited::GLRender(action). We have handled all rendering manually.
state->pop();
return;
}
if (ctx && ctx->highlightIndex == std::numeric_limits<int>::max()) {
if (ctx->selectionIndex.empty() || ctx->isSelectAll()) {
if (ctx2) {
@@ -478,6 +619,40 @@ void SoBrepEdgeSet::doAction(SoAction* action)
Gui::SoSelectionElementAction* selaction = static_cast<Gui::SoSelectionElementAction*>(action);
switch (selaction->getType()) {
case Gui::SoSelectionElementAction::Color:
if (selaction->isSecondary()) {
const auto& colors = selaction->getColors();
// Case 1: The color map is empty. This is a "clear" command.
if (colors.empty()) {
// We must find and remove any existing secondary context for this node.
if (Gui::SoFCSelectionRoot::removeActionContext(action, this)) {
touch();
}
return;
}
// Case 2: The color map is NOT empty. This is a "set color" command.
static std::string element("Edge");
bool hasEdgeColors = false;
for (const auto& [name, color] : colors) {
if (name.empty() || boost::starts_with(name, element)) {
hasEdgeColors = true;
break;
}
}
if (hasEdgeColors) {
auto ctx = Gui::SoFCSelectionRoot::getActionContext<SelContext>(action, this);
selCounter.checkAction(selaction, ctx);
ctx->selectAll();
if (ctx->setColors(colors, element)) {
touch();
}
}
}
return;
case Gui::SoSelectionElementAction::None: {
if (selaction->isSecondary()) {
if (Gui::SoFCSelectionRoot::removeActionContext(action, this)) {
@@ -490,6 +665,7 @@ void SoBrepEdgeSet::doAction(SoAction* action)
if (ctx) {
ctx->selectionIndex.clear();
ctx->sl.clear();
ctx->colors.clear();
touch();
}
}