From fe534bcf994a9718e76bf4ab649e31bba6c1ca97 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Fri, 3 Oct 2025 04:07:06 +0200 Subject: [PATCH] Assembly: Isolate joint components during selection and edit. (#23680) * Core: Add signalBeforeOpenTransaction * Assembly: Isolate * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update src/App/AutoTransaction.cpp Co-authored-by: Chris Hennes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Chris Hennes --- src/App/AutoTransaction.cpp | 2 +- .../panels/TaskAssemblyCreateJoint.ui | 14 ++ src/Mod/Assembly/Gui/ViewProviderAssembly.cpp | 194 +++++++++++++++++- src/Mod/Assembly/Gui/ViewProviderAssembly.h | 33 +++ src/Mod/Assembly/Gui/ViewProviderAssembly.pyi | 23 +++ .../Gui/ViewProviderAssemblyPyImp.cpp | 50 +++++ src/Mod/Assembly/JointObject.py | 41 ++++ 7 files changed, 354 insertions(+), 3 deletions(-) diff --git a/src/App/AutoTransaction.cpp b/src/App/AutoTransaction.cpp index d6c1e18d2b..d98356ef24 100644 --- a/src/App/AutoTransaction.cpp +++ b/src/App/AutoTransaction.cpp @@ -131,12 +131,12 @@ void AutoTransaction::setEnable(bool enable) int Application::setActiveTransaction(const char* name, bool persist) { + if (!name || !name[0]) { name = "Command"; } this->signalBeforeOpenTransaction(name); - if (_activeTransactionGuard > 0 && getActiveTransaction()) { if (_activeTransactionTmpName) { FC_LOG("transaction rename to '" << name << "'"); diff --git a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui index a422933907..18be75c632 100644 --- a/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui +++ b/src/Mod/Assembly/Gui/Resources/panels/TaskAssemblyCreateJoint.ui @@ -17,6 +17,20 @@ + + + + + + Isolate + + + + + + + + diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp index b4def4a83c..ba9ede7a83 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.cpp @@ -39,6 +39,9 @@ #include +#include +#include +#include #include #include @@ -57,6 +60,8 @@ #include #include #include +#include +#include #include #include @@ -111,9 +116,15 @@ ViewProviderAssembly::ViewProviderAssembly() , lastClickTime(0) , jointVisibilitiesBackup({}) , docsToMove({}) -{} +{ + m_preTransactionConn = App::GetApplication().signalBeforeOpenTransaction.connect( + std::bind(&ViewProviderAssembly::slotAboutToOpenTransaction, this, std::placeholders::_1)); +} -ViewProviderAssembly::~ViewProviderAssembly() = default; +ViewProviderAssembly::~ViewProviderAssembly() +{ + m_preTransactionConn.disconnect(); +}; QIcon ViewProviderAssembly::getIcon() const { @@ -1106,6 +1117,23 @@ void ViewProviderAssembly::draggerMotionCallback(void* data, SoDragger* d) void ViewProviderAssembly::onSelectionChanged(const Gui::SelectionChanges& msg) { + // Joint components isolation + if (msg.Type == Gui::SelectionChanges::AddSelection) { + auto selection = Gui::Selection().getSelection(); + if (selection.size() == 1) { + App::DocumentObject* obj = selection[0].pObject; + // A simple way to identify a joint is to check for its "JointType" property. + if (obj && obj->getPropertyByName("JointType")) { + isolateJointReferences(obj); + return; + } + } + } + if (msg.Type == Gui::SelectionChanges::ClrSelection + || msg.Type == Gui::SelectionChanges::RmvSelection) { + clearIsolate(); + } + if (!isInEditMode()) { return; } @@ -1264,6 +1292,168 @@ PyObject* ViewProviderAssembly::getPyObject() return pyViewObject; } +void ViewProviderAssembly::applyIsolationRecursively(App::DocumentObject* current, + std::set& isolateSet, + IsolateMode mode, + std::set& visited) +{ + if (!current || !visited.insert(current).second) { + return; // Object is null or already processed + } + + bool isolate = isolateSet.count(current); + + if (auto* group = dynamic_cast(current)) { + for (auto* child : group->Group.getValues()) { + applyIsolationRecursively(child, isolateSet, mode, visited); + } + } + else if (auto* part = dynamic_cast(current)) { + // As App::Part currently don't have material override + // (there is in LinkStage and RealThunder said he'll try to PR later) + // we have to recursively apply to children of App::Parts. + + // If Part is in isolateSet, then all its children should be added to isolateSet + if (isolate) { + for (auto* child : part->Group.getValues()) { + isolateSet.insert(child); + } + } + for (auto* child : part->Group.getValues()) { + applyIsolationRecursively(child, isolateSet, mode, visited); + } + } + + auto* vp = Gui::Application::Instance->getViewProvider(current); + auto* vpl = dynamic_cast(vp); + auto* vpg = dynamic_cast(vp); + if (!vpl && !vpg) { + return; // we process only geometric objects and links. + } + + // Backup the initial values. + ComponentState state; + state.visibility = current->Visibility.getValue(); + if (vpl) { + state.selectable = vpl->Selectable.getValue(); + state.overrideMaterial = vpl->OverrideMaterial.getValue(); + state.shapeMaterial = vpl->ShapeMaterial.getValue(); + } + else { // vpg + 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; + } + + if (isolate && !state.visibility) { // force visibility for isolated objects + 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); + } + } +} + +void ViewProviderAssembly::isolateComponents(std::set& isolateSet, + IsolateMode mode) +{ + if (!stateBackup.empty()) { + clearIsolate(); + } + + auto* assembly = getObject(); + if (!assembly) { + return; + } + + std::vector topLevelChildren = assembly->Group.getValues(); + + std::set visited; + for (auto* child : topLevelChildren) { + applyIsolationRecursively(child, isolateSet, mode, visited); + } +} + +void ViewProviderAssembly::isolateJointReferences(App::DocumentObject* joint, IsolateMode mode) +{ + if (!joint || isolatedJoint == joint) { + return; + } + + AssemblyObject* assembly = getObject(); + + App::DocumentObject* part1 = getMovingPartFromRef(assembly, joint, "Reference1"); + App::DocumentObject* part2 = getMovingPartFromRef(assembly, joint, "Reference2"); + if (!part1 || !part2) { + return; + } + + isolatedJoint = joint; + isolatedJointVisibilityBackup = joint->Visibility.getValue(); + joint->Visibility.setValue(true); + + std::set isolateSet = {part1, part2}; + isolateComponents(isolateSet, mode); +} + +void ViewProviderAssembly::clearIsolate() +{ + if (isolatedJoint) { + isolatedJoint->Visibility.setValue(isolatedJointVisibilityBackup); + isolatedJoint = nullptr; + } + + for (const auto& pair : stateBackup) { + App::DocumentObject* component = pair.first; + const ComponentState& state = pair.second; + if (!component || !component->isAttachedToDocument()) { + continue; + } + + component->Visibility.setValue(state.visibility); + + if (auto* vpl = dynamic_cast( + Gui::Application::Instance->getViewProvider(component))) { + 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))) { + vpg->Selectable.setValue(state.selectable); + vpg->ShapeAppearance.setValue(state.shapeMaterial); + } + } + + stateBackup.clear(); +} + +void ViewProviderAssembly::slotAboutToOpenTransaction(const std::string& cmdName) +{ + Q_UNUSED(cmdName); + this->clearIsolate(); +} + // UTILS Base::Vector3d ViewProviderAssembly::getCenterOfBoundingBox(const std::vector& movingObjs) diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.h b/src/Mod/Assembly/Gui/ViewProviderAssembly.h index 27db6ea3c3..66a707da36 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.h +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.h @@ -98,6 +98,13 @@ class AssemblyGuiExport ViewProviderAssembly: public Gui::ViewProviderPart, }; public: + enum class IsolateMode + { + Transparent, + Wireframe, + Hidden, + }; + ViewProviderAssembly(); ~ViewProviderAssembly() override; @@ -204,6 +211,11 @@ public: void UpdateSolverInformation(); + void isolateComponents(std::set& parts, IsolateMode mode); + void isolateJointReferences(App::DocumentObject* joint, + IsolateMode mode = IsolateMode::Transparent); + void clearIsolate(); + DragMode dragMode; bool canStartDragging; bool partMoving; @@ -246,8 +258,29 @@ private: App::DocumentObject* currentObject, bool onlySolids); + void slotAboutToOpenTransaction(const std::string& cmdName); + + struct ComponentState + { + bool visibility; + bool selectable; + // For Links + bool overrideMaterial; + App::Material shapeMaterial; + }; + + std::unordered_map stateBackup; + App::DocumentObject* isolatedJoint {nullptr}; + bool isolatedJointVisibilityBackup {false}; + + void applyIsolationRecursively(App::DocumentObject* current, + std::set& isolateSet, + IsolateMode mode, + std::set& visited); + TaskAssemblyMessages* taskSolver; boost::signals2::connection connectSolverUpdate; + boost::signals2::scoped_connection m_preTransactionConn; }; } // namespace AssemblyGui diff --git a/src/Mod/Assembly/Gui/ViewProviderAssembly.pyi b/src/Mod/Assembly/Gui/ViewProviderAssembly.pyi index 7d7e36313b..11ccd4ba1d 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssembly.pyi +++ b/src/Mod/Assembly/Gui/ViewProviderAssembly.pyi @@ -28,6 +28,29 @@ class ViewProviderAssembly(ViewProvider): Returns: dragger coin object of the assembly""" ... + + def isolateComponents( + self, components: List[DocumentObject] | Tuple[DocumentObject, ...], mode: int + ) -> None: + """ + Temporarily isolates a given set of components in the 3D view. + Other components are faded or hidden based on the specified mode. + + Args: + components (List[DocumentObject] | Tuple[DocumentObject, ...]): + A list or tuple of DocumentObjects to isolate. + mode (int): An integer specifying the isolation mode: + - 0: Transparent + - 1: Wireframe + - 2: Hidden + """ + ... + + def clearIsolate(self) -> None: + """ + Restores the visual state of all components, clearing any active isolation. + """ + ... EnableMovement: bool """Enable moving the parts by clicking and dragging.""" diff --git a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp index 0ebd0ff52c..fd1bdd6266 100644 --- a/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp +++ b/src/Mod/Assembly/Gui/ViewProviderAssemblyPyImp.cpp @@ -24,6 +24,7 @@ #include #include #include +#include // inclusion of the generated files (generated out of ViewProviderAssemblyPy.xml) #include "ViewProviderAssemblyPy.h" @@ -130,3 +131,52 @@ int ViewProviderAssemblyPy::setCustomAttributes(const char* /*attr*/, PyObject* { return 0; } + +PyObject* ViewProviderAssemblyPy::isolateComponents(PyObject* args) +{ + PyObject* pyList = nullptr; + int modeInt = 0; + if (!PyArg_ParseTuple(args, "Oi", &pyList, &modeInt)) { + return nullptr; + } + + if (!PySequence_Check(pyList)) { + PyErr_SetString(PyExc_TypeError, "First argument must be a sequence of DocumentObjects"); + return nullptr; + } + + if (modeInt < 0 || modeInt > 2) { + PyErr_SetString(PyExc_ValueError, "Mode must be an integer between 0 and 2"); + return nullptr; + } + + std::set partsSet; + Py_ssize_t size = PySequence_Size(pyList); + for (Py_ssize_t i = 0; i < size; ++i) { + PyObject* item = PySequence_GetItem(pyList, i); + if (item && PyObject_TypeCheck(item, &(App::DocumentObjectPy::Type))) { + auto* pyObj = static_cast(item); + App::DocumentObject* docObj = pyObj->getDocumentObjectPtr(); + if (docObj) { + partsSet.insert(docObj); + } + } + Py_XDECREF(item); + } + + auto mode = static_cast(modeInt); + getViewProviderAssemblyPtr()->isolateComponents(partsSet, mode); + + Py_DECREF(Py_None); + return Py_None; +} + +PyObject* ViewProviderAssemblyPy::clearIsolate(PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + getViewProviderAssemblyPtr()->clearIsolate(); + return Py::new_reference_to(Py::None()); +} diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 89d15f5562..b8f0b7c088 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -995,6 +995,8 @@ class ViewProviderJoint: return None def doubleClicked(self, vobj): + App.closeActiveTransaction(True) # Close the auto-transaction + task = Gui.Control.activeTaskDialog() if task: task.reject() @@ -1303,9 +1305,16 @@ class TaskAssemblyCreateJoint(QtCore.QObject): layout.setSpacing(0) layout.addWidget(self.jForm) + self.isolate_modes = ["Transparent", "Wireframe", "Hidden", "Disabled"] + self.jForm.isolateType.addItems( + [translate("Assembly", mode) for mode in self.isolate_modes] + ) + self.jForm.isolateType.currentIndexChanged.connect(self.updateIsolation) + if self.activeType == "Part": self.jForm.setWindowTitle("Match parts") self.jForm.jointType.hide() + self.jForm.isolateType.hide() self.jForm.jointType.addItems(TranslatedJointTypes) @@ -1667,6 +1676,35 @@ class TaskAssemblyCreateJoint(QtCore.QObject): UtilsAssembly.openEditingPlacementDialog(self.joint, "Offset2") self.updateOffsetWidgets() + def updateIsolation(self): + """Isolates the two selected components or clears isolation.""" + + if self.activeType != "Assembly": + return + + isolate_mode = self.jForm.isolateType.currentIndex() + + assembly_vobj = self.assembly.ViewObject + + # If "Disabled" is selected, clear any active isolation and stop. + if isolate_mode == 3: + assembly_vobj.clearIsolate() + return + + if len(self.refs) == 2: + try: + # Use a set to handle cases where both refs point to the same object + parts_to_isolate = { + UtilsAssembly.getObject(self.refs[0]), + UtilsAssembly.getObject(self.refs[1]), + } + assembly_vobj.isolateComponents(list(parts_to_isolate), isolate_mode) + except Exception as e: + App.Console.PrintWarning(f"Could not update isolation: {e}\n") + assembly_vobj.clearIsolate() + else: + assembly_vobj.clearIsolate() + def updateTaskboxFromJoint(self): self.refs = [] self.presel_ref = None @@ -1698,6 +1736,7 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.jForm.jointType.setCurrentIndex(JointTypes.index(self.joint.JointType)) self.updateJointList() + self.updateIsolation() def updateJoint(self): # First we build the listwidget @@ -1706,6 +1745,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): # Then we pass the new list to the joint object self.joint.Proxy.setJointConnectors(self.joint, self.refs) + self.updateIsolation() + def updateJointList(self): self.jForm.featureList.clear() simplified_names = []