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 = []