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:
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user