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 <chennes@pioneerlibrarysystem.org>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Chris Hennes <chennes@pioneerlibrarysystem.org>
This commit is contained in:
PaddleStroke
2025-10-03 04:07:06 +02:00
committed by GitHub
parent ce72f18eea
commit fe534bcf99
7 changed files with 354 additions and 3 deletions

View File

@@ -17,6 +17,20 @@
<item row="0" column="0" colspan="2">
<widget class="QComboBox" name="jointType"/>
</item>
<item row="15" column="0" colspan="2">
<layout class="QHBoxLayout" name="hLayoutIsolate">
<item>
<widget class="QLabel" name="isolateLabel2">
<property name="text">
<string>Isolate</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="isolateType"/>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QListWidget" name="featureList">
<property name="maximumSize">

View File

@@ -39,6 +39,9 @@
#include <chrono>
#include <set>
#include <algorithm>
#include <iterator>
#include <App/Link.h>
#include <App/Document.h>
@@ -57,6 +60,8 @@
#include <Gui/MainWindow.h>
#include <Gui/View3DInventor.h>
#include <Gui/View3DInventorViewer.h>
#include <Gui/ViewProviderLink.h>
#include <Gui/ViewProviderGeometryObject.h>
#include <Gui/ViewParams.h>
#include <Mod/Assembly/App/AssemblyLink.h>
@@ -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<App::DocumentObject*>& isolateSet,
IsolateMode mode,
std::set<App::DocumentObject*>& visited)
{
if (!current || !visited.insert(current).second) {
return; // Object is null or already processed
}
bool isolate = isolateSet.count(current);
if (auto* group = dynamic_cast<App::DocumentObjectGroup*>(current)) {
for (auto* child : group->Group.getValues()) {
applyIsolationRecursively(child, isolateSet, mode, visited);
}
}
else if (auto* part = dynamic_cast<App::Part*>(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<Gui::ViewProviderLink*>(vp);
auto* vpg = dynamic_cast<Gui::ViewProviderGeometryObject*>(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<App::DocumentObject*>& isolateSet,
IsolateMode mode)
{
if (!stateBackup.empty()) {
clearIsolate();
}
auto* assembly = getObject<AssemblyObject>();
if (!assembly) {
return;
}
std::vector<App::DocumentObject*> topLevelChildren = assembly->Group.getValues();
std::set<App::DocumentObject*> 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<AssemblyObject>();
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<App::DocumentObject*> 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::ViewProviderLink*>(
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::ViewProviderGeometryObject*>(
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<MovingObject>& movingObjs)

View File

@@ -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<App::DocumentObject*>& 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<App::DocumentObject*, ComponentState> stateBackup;
App::DocumentObject* isolatedJoint {nullptr};
bool isolatedJointVisibilityBackup {false};
void applyIsolationRecursively(App::DocumentObject* current,
std::set<App::DocumentObject*>& isolateSet,
IsolateMode mode,
std::set<App::DocumentObject*>& visited);
TaskAssemblyMessages* taskSolver;
boost::signals2::connection connectSolverUpdate;
boost::signals2::scoped_connection m_preTransactionConn;
};
} // namespace AssemblyGui

View File

@@ -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."""

View File

@@ -24,6 +24,7 @@
#include <Base/Interpreter.h>
#include <Base/PlacementPy.h>
#include <Base/GeometryPyCXX.h>
#include <App/DocumentObjectPy.h>
// 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<App::DocumentObject*> 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<App::DocumentObjectPy*>(item);
App::DocumentObject* docObj = pyObj->getDocumentObjectPtr();
if (docObj) {
partsSet.insert(docObj);
}
}
Py_XDECREF(item);
}
auto mode = static_cast<ViewProviderAssembly::IsolateMode>(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());
}

View File

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