From 436fef03ed213ea8bf564cf653db37f0e99433bf Mon Sep 17 00:00:00 2001 From: paddle Date: Fri, 31 Oct 2025 08:00:53 +0100 Subject: [PATCH 1/2] Assembly: Highlight joint elements on joint selection --- src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 119 ++++++++++++------ src/Mod/Assembly/Gui/ViewProviderAssembly.h | 3 + src/Mod/Part/Gui/SoBrepEdgeSet.cpp | 112 ++++++++++++++++- 3 files changed, 191 insertions(+), 43 deletions(-) diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 57907cfe94..81a9768622 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -42,6 +42,8 @@ #include #include #include +#include +#include #include #include @@ -63,6 +65,7 @@ #include #include #include +#include #include #include @@ -1372,17 +1375,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; } @@ -1391,39 +1390,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 inList = current->getInList(); - for (auto* child : inList) { - if (child->isDerivedFrom() && child->getLinkedObject() == current) { - // In this case we need to reverse isolate this! - auto* childVp = freecad_cast( - 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 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()); } } @@ -1466,6 +1449,8 @@ void ViewProviderAssembly::isolateJointReferences(App::DocumentObject* joint, Is std::set isolateSet = {part1, part2}; isolateComponents(isolateSet, mode); + + highlightJointElements(joint); } void ViewProviderAssembly::clearIsolate() @@ -1473,6 +1458,8 @@ void ViewProviderAssembly::clearIsolate() if (isolatedJoint) { isolatedJoint->Visibility.setValue(isolatedJointVisibilityBackup); isolatedJoint = nullptr; + + clearJointElementHighlight(); } for (const auto& pair : stateBackup) { @@ -1483,25 +1470,75 @@ void ViewProviderAssembly::clearIsolate() } component->Visibility.setValue(state.visibility); - - if (auto* vpl = dynamic_cast( - Gui::Application::Instance->getViewProvider(component) - )) { + auto* vp = Gui::Application::Instance->getViewProvider(component); + if (auto* vpl = dynamic_cast(vp)) { vpl->Selectable.setValue(state.selectable); - vpl->ShapeMaterial.setValue(state.shapeMaterial); - vpl->OverrideMaterial.setValue(state.overrideMaterial); } - else if (auto* vpg = dynamic_cast( - Gui::Application::Instance->getViewProvider(component) - )) { + else if (auto* vpg = dynamic_cast(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 processedElements; + + const char* refNames[] = {"Reference1", "Reference2"}; + for (const char* refName : refNames) { + auto* propRef = dynamic_cast(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(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 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); diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 76a2dabdd4..32ab6f8bbd 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -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& isolateSet, diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp index 7ed1d5f145..4c0b11e754 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp @@ -62,7 +62,7 @@ using namespace PartGui; SO_NODE_SOURCE(SoBrepEdgeSet) -struct SoBrepEdgeSet::SelContext: Gui::SoFCSelectionContext +struct SoBrepEdgeSet::SelContext: Gui::SoFCSelectionContextEx { std::vector hl, sl; }; @@ -86,7 +86,7 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction* action) SelContextPtr ctx2; SelContextPtr ctx = Gui::SoFCSelectionRoot::getRenderContext(this, selContext, ctx2); - if (ctx2 && ctx2->selectionIndex.empty()) { + if (ctx2 && ctx2->selectionIndex.empty() && ctx2->colors.empty()) { return; } @@ -128,6 +128,79 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction* action) ctx = selContext2; } + bool hasColorOverride = (ctx2 && !ctx2->colors.empty()); + if (hasColorOverride) { + 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; + this->getVertexData( + state, + coords, + normals, + cindices, + nindices, + tindices, + mindices, + numcindices, + false, + normalCacheUsed + ); + + SoMaterialBundle mb(action); + mb.sendFirst(); + + std::vector packedColors; + std::vector matIdx; + bool hasTransparency = false; + + // CORRECTED way to get default packed color + const SoLazyElement* lazyElem = SoLazyElement::getInstance(state); + packedColors.push_back( + lazyElem->getDiffuse(state, 0).getPackedValue(lazyElem->getTransparency(state, 0)) + ); + + int linecount = 0; + for (int i = 0; i < numcindices; i++) { + if (cindices[i] < 0) { + auto it = ctx2->colors.find(linecount); + if (it != ctx2->colors.end()) { + packedColors.push_back(ctx2->packColor(it->second, hasTransparency)); + matIdx.push_back(packedColors.size() - 1); + } + else { + auto it_all = ctx2->colors.find(-1); + if (it_all != ctx2->colors.end()) { + packedColors.push_back(ctx2->packColor(it_all->second, hasTransparency)); + matIdx.push_back(packedColors.size() - 1); + } + else { + matIdx.push_back(0); + } + } + linecount++; + } + } + + if (!matIdx.empty()) { + SoLazyElement::setPacked(state, this, packedColors.size(), packedColors.data(), hasTransparency); + SoMaterialBindingElement::set(state, this, SoMaterialBindingElement::PER_PART_INDEXED); + this->materialIndex.setValues(0, matIdx.size(), matIdx.data()); + } + + inherited::GLRender(action); + + this->materialIndex.deleteValues(0); + state->pop(); + return; + } + if (ctx && ctx->highlightIndex == std::numeric_limits::max()) { if (ctx->selectionIndex.empty() || ctx->isSelectAll()) { if (ctx2) { @@ -478,6 +551,40 @@ void SoBrepEdgeSet::doAction(SoAction* action) Gui::SoSelectionElementAction* selaction = static_cast(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(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 +597,7 @@ void SoBrepEdgeSet::doAction(SoAction* action) if (ctx) { ctx->selectionIndex.clear(); ctx->sl.clear(); + ctx->colors.clear(); touch(); } } From 26a9d26061b19910e8080d6f97e3a1f1d8009fd9 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Wed, 19 Nov 2025 12:22:59 +0100 Subject: [PATCH 2/2] SoBrepEdgeSet : Fix color override crash --- src/Mod/Part/Gui/SoBrepEdgeSet.cpp | 138 +++++++++++++++++++++-------- 1 file changed, 103 insertions(+), 35 deletions(-) diff --git a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp index 4c0b11e754..5100d8ffb1 100644 --- a/src/Mod/Part/Gui/SoBrepEdgeSet.cpp +++ b/src/Mod/Part/Gui/SoBrepEdgeSet.cpp @@ -130,6 +130,17 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction* action) 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; @@ -140,6 +151,8 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction* action) const int32_t* mindices; int numcindices; SbBool normalCacheUsed; + + // We request normals (true) because default lines need them for lighting this->getVertexData( state, coords, @@ -149,54 +162,109 @@ void SoBrepEdgeSet::GLRender(SoGLRenderAction* action) tindices, mindices, numcindices, - false, + 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(); - std::vector packedColors; - std::vector matIdx; - bool hasTransparency = false; - - // CORRECTED way to get default packed color - const SoLazyElement* lazyElem = SoLazyElement::getInstance(state); - packedColors.push_back( - lazyElem->getDiffuse(state, 0).getPackedValue(lazyElem->getTransparency(state, 0)) - ); + // 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 highlights; int linecount = 0; - for (int i = 0; i < numcindices; i++) { - if (cindices[i] < 0) { - auto it = ctx2->colors.find(linecount); - if (it != ctx2->colors.end()) { - packedColors.push_back(ctx2->packColor(it->second, hasTransparency)); - matIdx.push_back(packedColors.size() - 1); - } - else { - auto it_all = ctx2->colors.find(-1); - if (it_all != ctx2->colors.end()) { - packedColors.push_back(ctx2->packColor(it_all->second, hasTransparency)); - matIdx.push_back(packedColors.size() - 1); - } - else { - matIdx.push_back(0); - } - } - linecount++; + 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++; } - if (!matIdx.empty()) { - SoLazyElement::setPacked(state, this, packedColors.size(), packedColors.data(), hasTransparency); - SoMaterialBindingElement::set(state, this, SoMaterialBindingElement::PER_PART_INDEXED); - this->materialIndex.setValues(0, matIdx.size(), matIdx.data()); + // --- 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(); } - inherited::GLRender(action); - - this->materialIndex.deleteValues(0); + // Do NOT call inherited::GLRender(action). We have handled all rendering manually. state->pop(); return; }