diff --git a/app/gcompose/src/MainPanel.cpp b/app/gcompose/src/MainPanel.cpp index a5a0431..623360c 100644 --- a/app/gcompose/src/MainPanel.cpp +++ b/app/gcompose/src/MainPanel.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -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(m_firstPane->currentViewport())) { + viewport->GroupSelection(m_context); + } + }); } void MainPanel::setContext(uLib::ObjectsContext* context) { diff --git a/src/Vtk/CMakeLists.txt b/src/Vtk/CMakeLists.txt index 370f86c..fa22ba8 100644 --- a/src/Vtk/CMakeLists.txt +++ b/src/Vtk/CMakeLists.txt @@ -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) diff --git a/src/Vtk/vtkMultiSelectionProp.cpp b/src/Vtk/vtkMultiSelectionProp.cpp new file mode 100644 index 0000000..b6131b4 --- /dev/null +++ b/src/Vtk/vtkMultiSelectionProp.cpp @@ -0,0 +1,154 @@ +#include "vtkMultiSelectionProp.h" +#include +#include +#include +#include +#include +#include +#include +#include "Math/Transform.h" +#include "Vtk/Math/vtkDense.h" + +namespace uLib { +namespace Vtk { + +MultiSelectionProp::MultiSelectionProp() : Prop3D() { + this->SetInstanceName("Selection Group"); + m_PrevMatrix = vtkSmartPointer::New(); + m_GroupHighlightActor = vtkSmartPointer::New(); + + vtkNew 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& 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 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 invPrev; + vtkMatrix4x4::Invert(m_PrevMatrix, invPrev); + + vtkNew 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(content)) { + vtkNew memberWorldMatrix; + Matrix4fToVtk(tr->GetWorldMatrix(), memberWorldMatrix); + + vtkNew 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 diff --git a/src/Vtk/vtkMultiSelectionProp.h b/src/Vtk/vtkMultiSelectionProp.h new file mode 100644 index 0000000..0030c89 --- /dev/null +++ b/src/Vtk/vtkMultiSelectionProp.h @@ -0,0 +1,49 @@ +#ifndef ULIB_VTK_MULTISELECTIONPROP_H +#define ULIB_VTK_MULTISELECTIONPROP_H + +#include "uLibVtkInterface.h" +#include +#include +#include + +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& members); + const std::vector& GetMembers() const { return m_Members; } + + virtual void Update() override; + virtual void SyncFromVtk() override; + + virtual vtkProp* GetProp() override; + virtual vtkProp3D* GetProxyProp() override; + +private: + std::vector m_Members; + vtkSmartPointer m_PrevMatrix; + vtkSmartPointer m_GroupHighlightActor; +}; + +} // namespace Vtk +} // namespace uLib + +#endif // ULIB_VTK_MULTISELECTIONPROP_H diff --git a/src/Vtk/vtkObjectsContext.cpp b/src/Vtk/vtkObjectsContext.cpp index b73260b..1931f77 100644 --- a/src/Vtk/vtkObjectsContext.cpp +++ b/src/Vtk/vtkObjectsContext.cpp @@ -136,6 +136,10 @@ Prop3D *ObjectsContext::CreateProp3D(uLib::Object *obj) { if (!obj) return nullptr; + if (auto* p3d = dynamic_cast(obj)) { + return p3d; + } + if (auto *vox = dynamic_cast(obj)) { return new VoxImage(vox); } else if (auto *box = dynamic_cast(obj)) { diff --git a/src/Vtk/vtkViewport.cpp b/src/Vtk/vtkViewport.cpp index d037d58..2b72c15 100644 --- a/src/Vtk/vtkViewport.cpp +++ b/src/Vtk/vtkViewport.cpp @@ -36,6 +36,8 @@ #include "Vtk/Math/vtkCylinder.h" #include "Math/Transform.h" #include "Vtk/Math/vtkAssembly.h" +#include "vtkMultiSelectionProp.h" +#include 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::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(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(caller); auto* self = static_cast(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 diff --git a/src/Vtk/vtkViewport.h b/src/Vtk/vtkViewport.h index 180a5af..ead05c1 100644 --- a/src/Vtk/vtkViewport.h +++ b/src/Vtk/vtkViewport.h @@ -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 m_Prop3Ds; + std::vector m_SelectedProps; + MultiSelectionProp* m_MultiSelectionProp; std::map m_ObjectToProp3D; };