diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index 6b718e4655..5029c1296a 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -43,6 +43,8 @@ #include #include #include +#include +#include #include #include @@ -64,6 +66,7 @@ #include #include #include +#include #include #include @@ -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 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()); } } @@ -1497,6 +1480,8 @@ void ViewProviderAssembly::isolateJointReferences(App::DocumentObject* joint, Is std::set 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::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 c8e2e29915..9ac9499f7e 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..5100d8ffb1 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,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 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::max()) { if (ctx->selectionIndex.empty() || ctx->isSelectAll()) { if (ctx2) { @@ -478,6 +619,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 +665,7 @@ void SoBrepEdgeSet::doAction(SoAction* action) if (ctx) { ctx->selectionIndex.clear(); ctx->sl.clear(); + ctx->colors.clear(); touch(); } }