feat: implement MultiSelectionProp to support grouped object transformation and selection in Viewport

This commit is contained in:
AndreaRigoni
2026-04-10 20:42:24 +00:00
parent f8f92ebf3d
commit e320c932d2
7 changed files with 305 additions and 15 deletions

View File

@@ -13,6 +13,7 @@
#include <QPushButton>
#include <QMenu>
#include <QAction>
#include <QShortcut>
#include <QApplication>
#include <QFileDialog>
#include <QFileInfo>
@@ -104,6 +105,14 @@ MainPanel::MainPanel(QWidget* parent) : QWidget(parent), m_context(nullptr), m_m
m_rootSplitter->setSizes(sizes);
mainLayout->addWidget(m_rootSplitter, 1);
// Shortcuts
auto* groupShortcut = new QShortcut(QKeySequence("Ctrl+G"), this);
connect(groupShortcut, &QShortcut::activated, [this]() {
if (auto* viewport = qobject_cast<uLib::Vtk::QViewport*>(m_firstPane->currentViewport())) {
viewport->GroupSelection(m_context);
}
});
}
void MainPanel::setContext(uLib::ObjectsContext* context) {

View File

@@ -4,6 +4,7 @@ set(HEADERS uLibVtkInterface.h
vtkQViewport.h
vtkViewport.h
vtkObjectsContext.h
vtkMultiSelectionProp.h
)
set(SOURCES uLibVtkInterface.cxx
@@ -12,6 +13,7 @@ set(SOURCES uLibVtkInterface.cxx
vtkQViewport.cpp
vtkViewport.cpp
vtkObjectsContext.cpp
vtkMultiSelectionProp.cpp
)
## Pull in Math VTK wrappers (sets MATH_SOURCES / MATH_HEADERS)

View File

@@ -0,0 +1,154 @@
#include "vtkMultiSelectionProp.h"
#include <vtkActor.h>
#include <vtkPolyDataMapper.h>
#include <vtkCubeSource.h>
#include <vtkProperty.h>
#include <vtkMatrix4x4.h>
#include <vtkRenderer.h>
#include <vtkRendererCollection.h>
#include "Math/Transform.h"
#include "Vtk/Math/vtkDense.h"
namespace uLib {
namespace Vtk {
MultiSelectionProp::MultiSelectionProp() : Prop3D() {
this->SetInstanceName("Selection Group");
m_PrevMatrix = vtkSmartPointer<vtkMatrix4x4>::New();
m_GroupHighlightActor = vtkSmartPointer<vtkActor>::New();
vtkNew<vtkPolyDataMapper> mapper;
m_GroupHighlightActor->SetMapper(mapper);
m_GroupHighlightActor->GetProperty()->SetRepresentationToWireframe();
m_GroupHighlightActor->GetProperty()->SetColor(0.0, 1.0, 0.0); // Green for group
m_GroupHighlightActor->GetProperty()->SetLineWidth(2.0);
m_GroupHighlightActor->GetProperty()->SetLighting(0);
m_GroupHighlightActor->PickableOff();
}
MultiSelectionProp::~MultiSelectionProp() {
}
MultiSelectionProp* MultiSelectionProp::Clone() const {
auto* copy = new MultiSelectionProp();
copy->SetMembers(this->m_Members);
copy->SetInstanceName(this->GetInstanceName());
return copy;
}
void MultiSelectionProp::SetMembers(const std::vector<Prop3D*>& members) {
m_Members = members;
Update();
// Reset prev matrix to current highlight position
if (m_GroupHighlightActor->GetUserMatrix()) {
m_PrevMatrix->DeepCopy(m_GroupHighlightActor->GetUserMatrix());
} else {
m_PrevMatrix->Identity();
}
}
void MultiSelectionProp::Update() {
if (m_Members.empty()) {
m_GroupHighlightActor->VisibilityOff();
return;
}
m_GroupHighlightActor->VisibilityOn();
double combinedBounds[6] = {VTK_DOUBLE_MAX, VTK_DOUBLE_MIN,
VTK_DOUBLE_MAX, VTK_DOUBLE_MIN,
VTK_DOUBLE_MAX, VTK_DOUBLE_MIN};
for (auto* member : m_Members) {
if (vtkProp* prop = member->GetProp()) {
double* b = prop->GetBounds();
if (b) {
for (int i = 0; i < 3; ++i) {
if (b[2*i] < combinedBounds[2*i]) combinedBounds[2*i] = b[2*i];
if (b[2*i+1] > combinedBounds[2*i+1]) combinedBounds[2*i+1] = b[2*i+1];
}
}
}
}
if (combinedBounds[0] > combinedBounds[1]) return;
vtkNew<vtkCubeSource> cube;
double maxDim = std::max({combinedBounds[1]-combinedBounds[0],
combinedBounds[3]-combinedBounds[2],
combinedBounds[5]-combinedBounds[4]});
double pad = maxDim * 0.02;
if (pad < 1e-4) pad = 0.05;
cube->SetBounds(combinedBounds[0]-pad, combinedBounds[1]+pad,
combinedBounds[2]-pad, combinedBounds[3]+pad,
combinedBounds[4]-pad, combinedBounds[5]+pad);
cube->Update();
if (auto* mapper = vtkPolyDataMapper::SafeDownCast(m_GroupHighlightActor->GetMapper())) {
mapper->SetInputConnection(cube->GetOutputPort());
}
// The highlight actor itself should have identity user matrix initially,
// as it's defined in world space bounds.
m_GroupHighlightActor->SetUserMatrix(nullptr);
m_PrevMatrix->Identity();
// Ensure it's in the renderers
vtkRendererCollection* rens = this->GetRenderers();
rens->InitTraversal();
for (int i = 0; i < rens->GetNumberOfItems(); ++i) {
vtkRenderer* ren = rens->GetNextItem();
ren->AddActor(m_GroupHighlightActor);
}
}
void MultiSelectionProp::SyncFromVtk() {
if (m_Members.empty()) return;
vtkMatrix4x4* currentMatrix = m_GroupHighlightActor->GetUserMatrix();
if (!currentMatrix) return;
// Calculate Delta: currentMatrix * Inv(m_PrevMatrix)
vtkNew<vtkMatrix4x4> invPrev;
vtkMatrix4x4::Invert(m_PrevMatrix, invPrev);
vtkNew<vtkMatrix4x4> delta;
vtkMatrix4x4::Multiply4x4(currentMatrix, invPrev, delta);
// Apply delta to all members
for (auto* member : m_Members) {
if (auto* content = member->GetContent()) {
if (auto* tr = dynamic_cast<uLib::TRS*>(content)) {
vtkNew<vtkMatrix4x4> memberWorldMatrix;
Matrix4fToVtk(tr->GetWorldMatrix(), memberWorldMatrix);
vtkNew<vtkMatrix4x4> nextWorldMatrix;
vtkMatrix4x4::Multiply4x4(delta, memberWorldMatrix, nextWorldMatrix);
// Set the new world matrix.
// We need to calculate the new local matrix if there's a parent.
if (tr->GetParent()) {
Matrix4f invParentWorld = tr->GetParent()->GetWorldMatrix().inverse();
Matrix4f nextLocalMatrix = invParentWorld * VtkToMatrix4f(nextWorldMatrix);
tr->FromMatrix(nextLocalMatrix);
} else {
tr->FromMatrix(VtkToMatrix4f(nextWorldMatrix));
}
member->Update();
}
}
}
m_PrevMatrix->DeepCopy(currentMatrix);
}
vtkProp* MultiSelectionProp::GetProp() {
return m_GroupHighlightActor;
}
vtkProp3D* MultiSelectionProp::GetProxyProp() {
return m_GroupHighlightActor;
}
} // namespace Vtk
} // namespace uLib

View File

@@ -0,0 +1,49 @@
#ifndef ULIB_VTK_MULTISELECTIONPROP_H
#define ULIB_VTK_MULTISELECTIONPROP_H
#include "uLibVtkInterface.h"
#include <vector>
#include <vtkSmartPointer.h>
#include <vtkMatrix4x4.h>
class vtkActor;
class vtkProp;
class vtkProp3D;
namespace uLib {
namespace Vtk {
/**
* @class MultiSelectionProp
* @brief A proxy Prop3D that represents a group of selected Prop3Ds.
* It manages a combined highlight and propagates transformations to its members.
*/
class MultiSelectionProp : public Prop3D {
public:
uLibTypeMacro(MultiSelectionProp, Prop3D)
MultiSelectionProp();
virtual ~MultiSelectionProp();
/** @brief Creates a new instance that is a copy of this one's selection state. */
MultiSelectionProp* Clone() const;
void SetMembers(const std::vector<Prop3D*>& members);
const std::vector<Prop3D*>& GetMembers() const { return m_Members; }
virtual void Update() override;
virtual void SyncFromVtk() override;
virtual vtkProp* GetProp() override;
virtual vtkProp3D* GetProxyProp() override;
private:
std::vector<Prop3D*> m_Members;
vtkSmartPointer<vtkMatrix4x4> m_PrevMatrix;
vtkSmartPointer<vtkActor> m_GroupHighlightActor;
};
} // namespace Vtk
} // namespace uLib
#endif // ULIB_VTK_MULTISELECTIONPROP_H

View File

@@ -136,6 +136,10 @@ Prop3D *ObjectsContext::CreateProp3D(uLib::Object *obj) {
if (!obj)
return nullptr;
if (auto* p3d = dynamic_cast<Prop3D*>(obj)) {
return p3d;
}
if (auto *vox = dynamic_cast<uLib::Abstract::VoxImage *>(obj)) {
return new VoxImage(vox);
} else if (auto *box = dynamic_cast<uLib::ContainerBox *>(obj)) {

View File

@@ -36,6 +36,8 @@
#include "Vtk/Math/vtkCylinder.h"
#include "Math/Transform.h"
#include "Vtk/Math/vtkAssembly.h"
#include "vtkMultiSelectionProp.h"
#include <vtkRendererCollection.h>
namespace uLib {
namespace Vtk {
@@ -69,6 +71,7 @@ struct ViewportData {
Viewport::Viewport()
: pv(new ViewportData())
, m_GridAxis(Y)
, m_MultiSelectionProp(new MultiSelectionProp())
{
}
@@ -100,6 +103,10 @@ Viewport::~Viewport()
pv->m_CameraWidget->Off();
pv->m_CameraWidget->SetInteractor(nullptr);
}
if (m_MultiSelectionProp) {
delete m_MultiSelectionProp;
m_MultiSelectionProp = nullptr;
}
delete pv;
}
@@ -192,6 +199,11 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
pv->m_Renderer->SetLayer(0);
}
// Connect MultiSelectionProp
if (m_MultiSelectionProp) {
m_MultiSelectionProp->ConnectRenderer(pv->m_Renderer);
}
// Setup Handler Widget
if (!std::getenv("CTEST_PROJECT_NAME")) {
pv->m_HandlerWidget = vtkSmartPointer<HandlerWidget>::New();
@@ -206,8 +218,10 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
widgetInteractionCallback->SetClientData(this);
widgetInteractionCallback->SetCallback([](vtkObject*, unsigned long, void* clientdata, void*){
auto* self = static_cast<Viewport*>(clientdata);
for (auto* p : self->m_Prop3Ds) {
if (p->IsSelected()) {
if (self->m_SelectedProps.size() > 1 && self->m_MultiSelectionProp) {
self->m_MultiSelectionProp->SyncFromVtk();
} else {
for (auto* p : self->m_SelectedProps) {
p->SyncFromVtk();
}
}
@@ -222,6 +236,7 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
clickCallback->SetCallback([](vtkObject* caller, unsigned long, void* clientdata, void*){
auto* iren = static_cast<vtkRenderWindowInteractor*>(caller);
auto* self = static_cast<Viewport*>(clientdata);
bool multiSelect = iren->GetShiftKey() != 0;
int* pos = iren->GetEventPosition();
self->pv->m_Picker->Pick(pos[0], pos[1], 0, self->pv->m_Renderer);
@@ -291,7 +306,7 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
}
}
}
self->SelectProp3D(target);
self->SelectProp3D(target, multiSelect);
});
iren->AddObserver(vtkCommand::LeftButtonPressEvent, clickCallback);
@@ -446,6 +461,11 @@ void Viewport::RegisterProp3D(Prop3D* p, bool isPart) {
m_Prop3Ds.push_back(p);
p->ConnectRenderer(pv->m_Renderer);
// Ensure m_MultiSelectionProp also has the same renderers
if (m_MultiSelectionProp) {
m_MultiSelectionProp->GetRenderers()->AddItem(pv->m_Renderer);
}
// If it's a part of an assembly, we don't want to draw it twice.
// Assembly itself already draws its parts.
// But we need ConnectRenderer above to allow highliting and property updates.
@@ -497,28 +517,59 @@ void Viewport::ObserveContext(ObjectsContext* ctx) {
});
}
void Viewport::SelectProp3D(Prop3D* prop)
void Viewport::SelectProp3D(Prop3D* prop, bool multi)
{
for (auto* p : m_Prop3Ds) {
p->SetSelected(p == prop);
}
if (pv->m_HandlerWidget) {
if (multi) {
if (prop) {
vtkProp3D* prop3d = prop->GetProxyProp();
auto it = std::find(m_SelectedProps.begin(), m_SelectedProps.end(), prop);
if (it != m_SelectedProps.end()) {
prop->SetSelected(false);
m_SelectedProps.erase(it);
} else {
prop->SetSelected(true);
m_SelectedProps.push_back(prop);
}
}
} else {
for (auto* p : m_SelectedProps) {
p->SetSelected(false);
}
m_SelectedProps.clear();
if (prop) {
prop->SetSelected(true);
m_SelectedProps.push_back(prop);
}
}
// Update HandlerWidget
if (pv->m_HandlerWidget) {
if (m_SelectedProps.empty()) {
pv->m_HandlerWidget->SetEnabled(0);
pv->m_HandlerWidget->SetProp3D(nullptr);
if (m_MultiSelectionProp) m_MultiSelectionProp->SetMembers({});
} else if (m_SelectedProps.size() == 1) {
Prop3D* selected = m_SelectedProps[0];
vtkProp3D* prop3d = selected->GetProxyProp();
if (prop3d) {
pv->m_HandlerWidget->SetProp3D(prop3d);
pv->m_HandlerWidget->SetEnabled(1);
pv->m_HandlerWidget->PlaceWidget(prop3d->GetBounds()); //TODO: FIX !
pv->m_HandlerWidget->PlaceWidget(prop3d->GetBounds());
}
if (m_MultiSelectionProp) m_MultiSelectionProp->SetMembers({});
} else {
pv->m_HandlerWidget->SetEnabled(0);
pv->m_HandlerWidget->SetProp3D(nullptr);
// Multi-selection
if (m_MultiSelectionProp) {
m_MultiSelectionProp->SetMembers(m_SelectedProps);
vtkProp3D* proxy = m_MultiSelectionProp->GetProxyProp();
pv->m_HandlerWidget->SetProp3D(proxy);
pv->m_HandlerWidget->SetEnabled(1);
pv->m_HandlerWidget->PlaceWidget(proxy->GetBounds());
}
}
}
Render();
OnSelectionChanged(prop);
OnSelectionChanged(m_SelectedProps.empty() ? nullptr : m_SelectedProps.back());
}
void Viewport::SetGridVisible(bool visible)
@@ -657,5 +708,19 @@ void Viewport::UpdateGrid()
pv->m_Annotation->SetText(1, gridLabel);
}
void Viewport::GroupSelection(uLib::ObjectsContext* targetCtx) {
if (!targetCtx || m_SelectedProps.size() <= 1 || !m_MultiSelectionProp) return;
// Clone the current multi-selection proxy
MultiSelectionProp* group = m_MultiSelectionProp->Clone();
// Add it to the context
targetCtx->AddObject(group);
// Select the new group and clear multi-selection
m_SelectedProps.clear();
SelectProp3D(group);
}
} // namespace Vtk
} // namespace uLib

View File

@@ -29,6 +29,7 @@ namespace Vtk {
struct ViewportData;
class HandlerWidget;
class MultiSelectionProp;
class ObjectsContext;
/**
@@ -49,7 +50,11 @@ public:
// Prop3D / prop management
void AddProp3D(Prop3D &prop);
void RemoveProp3D(Prop3D &prop);
void SelectProp3D(Prop3D *prop);
/** @brief Selects a specific Prop3D. If multi is true, it toggles selection in a group. */
void SelectProp3D(Prop3D* target, bool multi = false);
/** @brief Creates a persistent Selection Group from the current multi-selection. */
void GroupSelection(uLib::ObjectsContext* targetCtx);
void addProp(vtkProp *prop);
void RemoveProp(vtkProp *prop);
@@ -91,6 +96,8 @@ protected:
struct ViewportData *pv;
Axis m_GridAxis;
std::vector<Prop3D*> m_Prop3Ds;
std::vector<Prop3D*> m_SelectedProps;
MultiSelectionProp* m_MultiSelectionProp;
std::map<uLib::Object*, Prop3D*> m_ObjectToProp3D;
};