diff --git a/src/Core/ObjectsContext.h b/src/Core/ObjectsContext.h index a548d42..4264e61 100644 --- a/src/Core/ObjectsContext.h +++ b/src/Core/ObjectsContext.h @@ -20,13 +20,13 @@ public: * @brief Adds an object to the context. * @param obj Pointer to the object to add. */ - void AddObject(Object* obj); + virtual void AddObject(Object* obj); /** * @brief Removes an object from the context. * @param obj Pointer to the object to remove. */ - void RemoveObject(Object* obj); + virtual void RemoveObject(Object* obj); /** * @brief Clears all objects from the context. diff --git a/src/Math/Assembly.cpp b/src/Math/Assembly.cpp new file mode 100644 index 0000000..1966fdd --- /dev/null +++ b/src/Math/Assembly.cpp @@ -0,0 +1,146 @@ +/*////////////////////////////////////////////////////////////////////////////// +// CMT Cosmic Muon Tomography project ////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova + All rights reserved + + Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it > + +//////////////////////////////////////////////////////////////////////////////*/ + +#include "Math/Assembly.h" +#include "Math/ContainerBox.h" +#include "Math/Cylinder.h" + +#include +#include +#include + +namespace uLib { + +Assembly::Assembly() + : ObjectsContext(), + AffineTransform(), + m_BBoxMin(Vector3f::Zero()), + m_BBoxMax(Vector3f::Zero()), + m_ShowBoundingBox(false), + m_GroupSelection(true) {} + +Assembly::Assembly(const Assembly ©) + : ObjectsContext(copy), + AffineTransform(copy), + m_BBoxMin(copy.m_BBoxMin), + m_BBoxMax(copy.m_BBoxMax), + m_ShowBoundingBox(copy.m_ShowBoundingBox), + m_GroupSelection(copy.m_GroupSelection) {} + +Assembly::~Assembly() {} + +void Assembly::AddObject(Object *obj) { + if (auto *at = dynamic_cast(obj)) { + at->SetParent(this); + } + ObjectsContext::AddObject(obj); +} + +void Assembly::RemoveObject(Object *obj) { + if (auto *at = dynamic_cast(obj)) { + if (at->GetParent() == this) + at->SetParent(nullptr); + } + ObjectsContext::RemoveObject(obj); +} + +void Assembly::ComputeBoundingBox() { + const auto &objects = this->GetObjects(); + if (objects.empty()) { + m_BBoxMin = Vector3f::Zero(); + m_BBoxMax = Vector3f::Zero(); + return; + } + + float inf = std::numeric_limits::max(); + m_BBoxMin = Vector3f(inf, inf, inf); + m_BBoxMax = Vector3f(-inf, -inf, -inf); + + Matrix4f invAsm = this->GetWorldMatrix().inverse(); + + for (Object *obj : objects) { + if (auto *box = dynamic_cast(obj)) { + // ContainerBox: wm is matrix from unit cube [0,1] to assembly base + Matrix4f m = invAsm * box->GetWorldMatrix(); + for (int i = 0; i < 8; ++i) { + float x = (i & 1) ? 1.0f : 0.0f; + float y = (i & 2) ? 1.0f : 0.0f; + float z = (i & 4) ? 1.0f : 0.0f; + Vector4f corner = m * Vector4f(x, y, z, 1.0f); + for (int a = 0; a < 3; ++a) { + m_BBoxMin(a) = std::min(m_BBoxMin(a), corner(a)); + m_BBoxMax(a) = std::max(m_BBoxMax(a), corner(a)); + } + } + } else if (auto *cyl = dynamic_cast(obj)) { + // Cylinder: centered [-1, 1] radial, [-0.5, 0.5] height + Matrix4f m = invAsm * cyl->GetWorldMatrix(); + for (int i = 0; i < 8; ++i) { + float x = (i & 1) ? 1.0f : -1.0f; + float y = (i & 2) ? 0.5f : -0.5f; + float z = (i & 4) ? 1.0f : -1.0f; + Vector4f corner = m * Vector4f(x, y, z, 1.0f); + for (int a = 0; a < 3; ++a) { + m_BBoxMin(a) = std::min(m_BBoxMin(a), corner(a)); + m_BBoxMax(a) = std::max(m_BBoxMax(a), corner(a)); + } + } + } else if (auto *subAsm = dynamic_cast(obj)) { + // Recursive AABB for nested assemblies + subAsm->ComputeBoundingBox(); + Vector3f subMin, subMax; + subAsm->GetBoundingBox(subMin, subMax); + Matrix4f m = invAsm * subAsm->GetWorldMatrix(); + for (int i = 0; i < 8; ++i) { + float x = (i & 1) ? subMax(0) : subMin(0); + float y = (i & 2) ? subMax(1) : subMin(1); + float z = (i & 4) ? subMax(2) : subMin(2); + Vector4f corner = m * Vector4f(x, y, z, 1.0f); + for (int a = 0; a < 3; ++a) { + m_BBoxMin(a) = std::min(m_BBoxMin(a), corner(a)); + m_BBoxMax(a) = std::max(m_BBoxMax(a), corner(a)); + } + } + } + } +} + +void Assembly::GetBoundingBox(Vector3f &bbMin, Vector3f &bbMax) const { + bbMin = m_BBoxMin; + bbMax = m_BBoxMax; +} + +ContainerBox Assembly::GetBoundingBoxAsContainer() const { + ContainerBox bb; + Vector3f size = m_BBoxMax - m_BBoxMin; + bb.SetSize(size); + bb.SetPosition(m_BBoxMin); + return bb; +} + +void Assembly::SetShowBoundingBox(bool show) { + m_ShowBoundingBox = show; + this->Updated(); +} + +bool Assembly::GetShowBoundingBox() const { + return m_ShowBoundingBox; +} + +void Assembly::SetGroupSelection(bool group) { + m_GroupSelection = group; +} + +bool Assembly::GetGroupSelection() const { + return m_GroupSelection; +} + +} // namespace uLib diff --git a/src/Math/Assembly.h b/src/Math/Assembly.h new file mode 100644 index 0000000..7440062 --- /dev/null +++ b/src/Math/Assembly.h @@ -0,0 +1,109 @@ +/*////////////////////////////////////////////////////////////////////////////// +// CMT Cosmic Muon Tomography project ////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova + All rights reserved + + Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it > + + ------------------------------------------------------------------ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3.0 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. + +//////////////////////////////////////////////////////////////////////////////*/ + +#ifndef U_ASSEMBLY_H +#define U_ASSEMBLY_H + +#include "Core/ObjectsContext.h" +#include "Math/ContainerBox.h" +#include "Math/Transform.h" + +namespace uLib { + +/** + * @brief Assembly groups geometric objects (ContainerBox, Cylinder, etc.) + * under a common transformation. + * + * Assembly derives from ObjectsContext so objects can be added/removed + * dynamically. It also inherits AffineTransform to provide a group-level + * transformation that is applied on top of each child's own transform. + * + * A bounding box is automatically computed from all contained objects and + * can be queried or shown/hidden through the VTK puppet. + */ +class Assembly : public ObjectsContext, public AffineTransform { +public: + virtual const char *GetClassName() const override { return "Assembly"; } + + Assembly(); + Assembly(const Assembly ©); + virtual ~Assembly(); + + virtual void AddObject(Object* obj) override; + virtual void RemoveObject(Object* obj) override; + + /** + * @brief Recomputes the axis-aligned bounding box enclosing all children. + * Stores the result internally. + */ + void ComputeBoundingBox(); + + /** + * @brief Returns the bounding box as min/max corners (in assembly-local + * coordinates). + */ + void GetBoundingBox(Vector3f &bbMin, Vector3f &bbMax) const; + + /** + * @brief Returns the bounding box as a ContainerBox (positioned + * at bbMin, sized bbMax-bbMin, parented to this transform). + */ + ContainerBox GetBoundingBoxAsContainer() const; + + /** + * @brief Controls whether the bounding box wireframe should be shown + * in the viewer (used by the VTK puppet). + */ + void SetShowBoundingBox(bool show); + bool GetShowBoundingBox() const; + + /** + * @brief Controls selection behavior. + * If true (default), clicking any child within the assembly will select + * the assembly itself. If false, individual children can be picked. + */ + void SetGroupSelection(bool group); + bool GetGroupSelection() const; + +signals: + virtual void Updated() override { + if (m_InUpdated) return; // break signal recursion + m_InUpdated = true; + this->ComputeBoundingBox(); + ULIB_SIGNAL_EMIT(Assembly::Updated); + m_InUpdated = false; + } + +private: + Vector3f m_BBoxMin; + Vector3f m_BBoxMax; + bool m_ShowBoundingBox; + bool m_GroupSelection; + bool m_InUpdated = false; +}; + +} // namespace uLib + +#endif // U_ASSEMBLY_H diff --git a/src/Math/CMakeLists.txt b/src/Math/CMakeLists.txt index 3121b33..a14a1bc 100644 --- a/src/Math/CMakeLists.txt +++ b/src/Math/CMakeLists.txt @@ -1,6 +1,7 @@ set(HEADERS ContainerBox.h Cylinder.h + Assembly.h Dense.h Geometry.h Transform.h @@ -33,6 +34,7 @@ set(SOURCES VoxRaytracer.cpp VoxImage.cpp TriangleMesh.cpp QuadMesh.cpp + Assembly.cpp Dense.cpp Structured2DGrid.cpp Structured4DGrid.cpp diff --git a/src/Math/Transform.h b/src/Math/Transform.h index 232ed75..73cfacd 100644 --- a/src/Math/Transform.h +++ b/src/Math/Transform.h @@ -69,6 +69,8 @@ public: m_Parent(NULL) {} + virtual ~AffineTransform() {} + AffineTransform(AffineTransform *parent) : m_T(Matrix4f::Identity()), m_Parent(parent) diff --git a/src/Vtk/Math/CMakeLists.txt b/src/Vtk/Math/CMakeLists.txt index c722d94..0dbe4c3 100644 --- a/src/Vtk/Math/CMakeLists.txt +++ b/src/Vtk/Math/CMakeLists.txt @@ -10,6 +10,7 @@ set(MATH_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/vtkVoxImage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/vtkContainerBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/vtkCylinder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/vtkAssembly.cpp PARENT_SCOPE) set(MATH_HEADERS @@ -20,6 +21,7 @@ set(MATH_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/vtkVoxImage.h ${CMAKE_CURRENT_SOURCE_DIR}/vtkContainerBox.h ${CMAKE_CURRENT_SOURCE_DIR}/vtkCylinder.h + ${CMAKE_CURRENT_SOURCE_DIR}/vtkAssembly.h PARENT_SCOPE) if(BUILD_TESTING) diff --git a/src/Vtk/Math/testing/CMakeLists.txt b/src/Vtk/Math/testing/CMakeLists.txt index 04eaec3..a0a6edf 100644 --- a/src/Vtk/Math/testing/CMakeLists.txt +++ b/src/Vtk/Math/testing/CMakeLists.txt @@ -7,6 +7,7 @@ set(TESTS vtkVoxImageInteractiveTest vtkContainerBoxTest vtkContainerBoxTest2 + vtkAssemblyTest ) set(LIBRARIES diff --git a/src/Vtk/Math/testing/vtkAssemblyTest.cpp b/src/Vtk/Math/testing/vtkAssemblyTest.cpp index 713deb2..77bffb3 100644 --- a/src/Vtk/Math/testing/vtkAssemblyTest.cpp +++ b/src/Vtk/Math/testing/vtkAssemblyTest.cpp @@ -1,109 +1,104 @@ /*////////////////////////////////////////////////////////////////////////////// // CMT Cosmic Muon Tomography project ////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// -// Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova -// All rights reserved + + Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova + All rights reserved + + Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it > + //////////////////////////////////////////////////////////////////////////////*/ -#include "Vtk/uLibVtkViewer.h" -#include "Vtk/Math/vtkAssembly.h" -#include "Vtk/Math/vtkCylinder.h" -#include "Vtk/Math/vtkContainerBox.h" #include "Math/Assembly.h" -#include "Math/Cylinder.h" #include "Math/ContainerBox.h" +#include "Math/Cylinder.h" +#include "Vtk/Math/vtkAssembly.h" +#include "Vtk/vtkObjectsContext.h" +#include "Vtk/uLibVtkViewer.h" #include "Math/Units.h" +#include +#include +#include + #include using namespace uLib; -/** - * @brief This test verifies that uLib::Vtk::Assembly correctly visualizes a collection - * of objects and that transformations applied to the assembly are propagated to its children. - * It also checks that the assembly appears as a bounding box of its contents. - */ -int main() { - std::cout << "Starting vtkAssemblyTest..." << std::endl; +int main(int argc, char **argv) { + bool interactive = (argc > 1 && std::string(argv[1]) == "-i"); - // 1. Setup Core Geometry - std::cout << " - Creating core Assembly..." << std::endl; - uLib::Assembly* core_assembly = new uLib::Assembly(); - core_assembly->SetInstanceName("MainAssembly"); + // ---- 1. Build model objects ---- + ContainerBox box1; + box1.Scale(Vector3f(1_m, 2_m, 0.5_m)); + box1.SetPosition(Vector3f(0, 0, 0)); - // Add a box - std::cout << " - Adding ChildBox (Red)..." << std::endl; - ContainerBox* box = new ContainerBox(); - box->SetInstanceName("ChildBox"); - box->Translate(Vector3f(1.0, 0, 0)); - core_assembly->AddObject(box); + ContainerBox box2; + box2.Scale(Vector3f(0.5_m, 0.5_m, 3_m)); + box2.SetPosition(Vector3f(2_m, 0, 0)); - // Add a cylinder - // std::cout << " - Adding ChildCylinder (Blue)..." << std::endl; - // Cylinder* cyl = new Cylinder(0.5, 2.0); - // cyl->SetInstanceName("ChildCylinder"); - // cyl->Translate(Vector3f(-2.0, 0, 0)); - // core_assembly->AddObject(cyl); + Cylinder cyl(0.3_m, 1.5_m, 1); + cyl.SetPosition(Vector3f(0, 3_m, 0)); - std::cout << " - Adding another box (Green)..." << std::endl; - ContainerBox* box2 = new ContainerBox(); - box2->Scale(Vector3f(1.0, 1.0, 2.0)); - box2->SetInstanceName("ChildBox2"); - box2->Translate(Vector3f(0, 0, 1.0)); - core_assembly->AddObject(box2); + // ---- 2. Create an Assembly and add objects ---- + Assembly assembly; + assembly.AddObject(&box1); + assembly.AddObject(&box2); + assembly.AddObject(&cyl); + assembly.SetShowBoundingBox(true); - // 2. Setup VTK Representation - std::cout << " - Creating VTK Assembly representation..." << std::endl; - Vtk::Assembly vtk_assembly(core_assembly); - - // Find and colorized the children puppets - Vtk::Puppet* p_box = vtk_assembly.GetPuppet(box); - if (p_box) { - p_box->SetColor(1.0, 0.0, 0.0); // Red - } else { - std::cerr << "Warning: Could not find puppet for box!" << std::endl; - } + // ---- 3. Apply a group transform ---- + assembly.SetPosition(Vector3f(1_m, 1_m, 0)); - // Vtk::Puppet* p_cyl = vtk_assembly.GetPuppet(cyl); - // if (p_cyl) { - // p_cyl->SetColor(0.0, 0.0, 1.0); // Blue - // } else { - // std::cerr << "Warning: Could not find puppet for cylinder!" << std::endl; - // } - - Vtk::Puppet* p_box2 = vtk_assembly.GetPuppet(box2); - if (p_box2) { - p_box2->SetColor(0.0, 1.0, 0.0); // Green - } else { - std::cerr << "Warning: Could not find puppet for box2!" << std::endl; - } - - // 3. Test Transformation Propagation - std::cout << " - Rotating Assembly 45 degrees around Z..." << std::endl; - core_assembly->Rotate(45.0_deg, Vector3f::UnitZ()); - - std::cout << " - Translating Assembly up (+Y)..." << std::endl; - core_assembly->Translate(Vector3f(0, 2.0, 0)); - - // Notify all puppets of the change - core_assembly->Updated(); - - // 4. Run Visualization - std::cout << "Starting viewer (close to exit)..." << std::endl; - std::cout << "Expected: A white bounding box containing a red box and a blue cylinder, " - << "all rotated and translated as a group." << std::endl; + // ---- 5. Visualize (create puppets to set properties) ---- + Vtk::Assembly vtkAsm(&assembly); Vtk::Viewer viewer; - viewer.AddPuppet(*p_box); - viewer.AddPuppet(*p_box2); - viewer.AddPuppet(vtk_assembly); - viewer.Start(); + vtkAsm.AddToViewer(viewer); // This triggers puppet creation via ConnectRenderer which eventually calls Puppet::GetProp + + // Explicitly update to ensure puppets exist and are added to assemblies + vtkAsm.Update(); - // Clean up - delete core_assembly; - delete box; - delete box2; - // delete cyl; + // Use the child context to find child puppets and set colors + if (auto* childCtx = vtkAsm.GetChildrenContext()) { + auto setProps = [](Vtk::Puppet* p, float r, float g, float b) { + if (!p) return; + vtkPropCollection* props = p->GetProps(); + props->InitTraversal(); + for (int i=0; i < props->GetNumberOfItems(); ++i) { + if (auto* actor = vtkActor::SafeDownCast(props->GetNextProp())) { + actor->GetProperty()->SetColor(r, g, b); + actor->GetProperty()->SetRepresentationToSurface(); + actor->GetProperty()->SetOpacity(0.5); + } + } + }; + + setProps(childCtx->GetPuppet(&box1), 1.0, 0.0, 0.0); // Red + setProps(childCtx->GetPuppet(&box2), 0.0, 1.0, 0.0); // Green + setProps(childCtx->GetPuppet(&cyl), 0.0, 0.0, 1.0); // Blue + } + + std::cout << "Puppets in viewport: " << viewer.getPuppets().size() << " (Expected 4: 1 assembly + 3 children)" << std::endl; + + // ---- 4. Query the bounding box for terminal output ---- + Vector3f bbMin, bbMax; + assembly.GetBoundingBox(bbMin, bbMax); + std::cout << "Assembly bounding box:" << std::endl; + std::cout << " min = " << bbMin.transpose() << std::endl; + std::cout << " max = " << bbMax.transpose() << std::endl; + + std::cout << "==================================================\n"; + std::cout << " vtkAssemblyTest\n"; + std::cout << " 2 boxes + 1 cylinder grouped in an assembly\n"; + std::cout << "==================================================" << std::endl; + + if (interactive) { + viewer.ZoomAuto(); + viewer.Start(); + } else { + std::cout << "Non-interactive test passed." << std::endl; + } return 0; } diff --git a/src/Vtk/Math/vtkAssembly.cpp b/src/Vtk/Math/vtkAssembly.cpp new file mode 100644 index 0000000..19a3fd7 --- /dev/null +++ b/src/Vtk/Math/vtkAssembly.cpp @@ -0,0 +1,210 @@ +/*////////////////////////////////////////////////////////////////////////////// +// CMT Cosmic Muon Tomography project ////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova + All rights reserved + + Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it > + +//////////////////////////////////////////////////////////////////////////////*/ + +#include "vtkAssembly.h" // uLib::Vtk::Assembly +#include "Vtk/vtkObjectsContext.h" +#include "Math/vtkDense.h" + +#include // VTK library ::vtkAssembly +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace uLib { +namespace Vtk { + +// ------------------------------------------------------------------ // +Assembly::Assembly(uLib::Assembly *content) + : m_Content(content), + m_ChildContext(nullptr), + m_BBoxActor(nullptr), + m_VtkAsm(nullptr), + m_InUpdate(false), + m_BlockUpdate(false) { + this->InstallPipe(); + if (m_Content) { + Object::connect(m_Content, &uLib::Assembly::Updated, + this, &Assembly::contentUpdate); + } +} + +Assembly::~Assembly() { + delete m_ChildContext; + if (m_BBoxActor) m_BBoxActor->Delete(); + if (m_VtkAsm) m_VtkAsm->Delete(); +} + +// ------------------------------------------------------------------ // +void Assembly::InstallPipe() { + // 1. Create the VTK library assembly that groups everything + m_VtkAsm = ::vtkAssembly::New(); + this->SetProp(m_VtkAsm); + + // 2. Create the bounding-box wireframe actor + vtkNew cube; + cube->SetBounds(0, 1, 0, 1, 0, 1); + cube->Update(); + + vtkNew mapper; + mapper->SetInputConnection(cube->GetOutputPort()); + + m_BBoxActor = vtkActor::New(); + m_BBoxActor->SetMapper(mapper); + m_BBoxActor->GetProperty()->SetRepresentationToWireframe(); + m_BBoxActor->GetProperty()->SetColor(1.0, 0.85, 0.0); // gold wireframe + m_BBoxActor->GetProperty()->SetLineWidth(1.5); + m_BBoxActor->GetProperty()->SetOpacity(0.6); + m_BBoxActor->SetVisibility(m_Content ? m_Content->GetShowBoundingBox() : false); + + m_VtkAsm->AddPart(m_BBoxActor); + + // 3. Build a child-objects context (auto-creates puppets for each child) + if (m_Content) { + m_ChildContext = new vtkObjectsContext(m_Content); + // The vtkObjectsContext's own prop is already a ::vtkAssembly; + // nest it inside ours so everything moves together. + if (auto *childProp = vtkProp3D::SafeDownCast(m_ChildContext->GetProp())) + m_VtkAsm->AddPart(childProp); + } + + // 4. Apply initial transform + this->UpdateTransform(); + this->UpdateBoundingBox(); +} + +// ------------------------------------------------------------------ // +void Assembly::contentUpdate() { + if (m_InUpdate) return; + m_InUpdate = true; + + this->UpdateTransform(); + this->UpdateBoundingBox(); + if (m_ChildContext) + m_ChildContext->Update(); + + m_BlockUpdate = true; + Puppet::Update(); + m_InUpdate = false; +} + +// ------------------------------------------------------------------ // +void Assembly::Update() { + if (m_InUpdate) return; + if (!m_Content || !m_VtkAsm) return; + + if (m_BlockUpdate) { + m_BlockUpdate = false; + return; + } + + m_InUpdate = true; + + // Pull VTK transform back into the uLib model + vtkMatrix4x4* vmat = m_VtkAsm->GetUserMatrix(); + if (vmat) { + Matrix4f transform = VtkToMatrix4f(vmat); + m_Content->SetMatrix(transform); + } + + this->UpdateBoundingBox(); + if (m_ChildContext) + m_ChildContext->Update(); + + m_Content->Updated(); // Notify change in model + + m_InUpdate = false; +} + +// ------------------------------------------------------------------ // +void Assembly::UpdateTransform() { + if (!m_Content || !m_VtkAsm) return; + + Matrix4f mat = m_Content->GetMatrix(); + vtkNew vmat; + Matrix4fToVtk(mat, vmat); + m_VtkAsm->SetUserMatrix(vmat); + m_VtkAsm->Modified(); +} + +// ------------------------------------------------------------------ // +void Assembly::UpdateBoundingBox() { + if (!m_Content || !m_BBoxActor) return; + + m_BBoxActor->SetVisibility(m_Content->GetShowBoundingBox()); + + Vector3f bbMin, bbMax; + m_Content->GetBoundingBox(bbMin, bbMax); + + // Avoid degenerate boxes + Vector3f size = bbMax - bbMin; + if (size.norm() < 1e-6f) return; + + // Rebuild the corner segments to match the AABB + vtkNew points; + vtkNew lines; + + float bounds[2][3] = { + {bbMin(0), bbMin(1), bbMin(2)}, + {bbMax(0), bbMax(1), bbMax(2)} + }; + + // Corner segment length: 10% of dimension + float len[3] = { + (bbMax(0) - bbMin(0)) * 0.1f, + (bbMax(1) - bbMin(1)) * 0.1f, + (bbMax(2) - bbMin(2)) * 0.1f + }; + + for (int i = 0; i < 8; ++i) { + float p[3]; + p[0] = bounds[(i & 1) ? 1 : 0][0]; + p[1] = bounds[(i & 2) ? 1 : 0][1]; + p[2] = bounds[(i & 4) ? 1 : 0][2]; + + for (int axis = 0; axis < 3; ++axis) { + float p2[3] = {p[0], p[1], p[2]}; + float delta = (i & (1 << axis)) ? -len[axis] : len[axis]; + p2[axis] += delta; + + vtkIdType id1 = points->InsertNextPoint(p); + vtkIdType id2 = points->InsertNextPoint(p2); + lines->InsertNextCell(2); + lines->InsertCellPoint(id1); + lines->InsertCellPoint(id2); + } + } + + vtkNew poly; + poly->SetPoints(points); + poly->SetLines(lines); + + vtkNew mapper; + mapper->SetInputData(poly); + + m_BBoxActor->SetMapper(mapper); + m_BBoxActor->Modified(); +} + +// ------------------------------------------------------------------ // +vtkObjectsContext *Assembly::GetChildrenContext() const { + return m_ChildContext; +} + +} // namespace Vtk +} // namespace uLib diff --git a/src/Vtk/Math/vtkAssembly.h b/src/Vtk/Math/vtkAssembly.h new file mode 100644 index 0000000..297a9c0 --- /dev/null +++ b/src/Vtk/Math/vtkAssembly.h @@ -0,0 +1,71 @@ +/*////////////////////////////////////////////////////////////////////////////// +// CMT Cosmic Muon Tomography project ////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova + All rights reserved + + Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it > + +//////////////////////////////////////////////////////////////////////////////*/ + +#ifndef U_VTK_ASSEMBLY_H +#define U_VTK_ASSEMBLY_H + +#include "Math/Assembly.h" +#include "uLibVtkInterface.h" + +class vtkActor; +class vtkAssembly; // VTK library forward declaration (must be before namespace) + +namespace uLib { +namespace Vtk { + +class vtkObjectsContext; // forward + +/** + * @brief VTK Puppet for visualizing uLib::Assembly. + * + * Manages a VTK assembly (vtkAssembly from the VTK library) that groups + * all child puppets and applies the Assembly's AffineTransform. It also + * renders an optional bounding box wireframe computed from the Assembly's AABB. + * + * @note This class is uLib::Vtk::Assembly. It internally uses + * the VTK library class vtkAssembly for grouping, but the two + * are distinct. + */ +class Assembly : public Puppet { +public: + virtual const char *GetClassName() const override { return "Vtk.Assembly"; } + + Assembly(uLib::Assembly *content); + virtual ~Assembly(); + + /** @brief Updates the VTK representation from the model (model→VTK). */ + virtual void Update() override; + + virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; } + + /** @brief Called when the model signals an update (model→VTK push). */ + void contentUpdate(); + + /** @brief Returns the puppet managing child objects. */ + vtkObjectsContext *GetChildrenContext() const; + +private: + void UpdateTransform(); + void UpdateBoundingBox(); + void InstallPipe(); + + uLib::Assembly *m_Content; + vtkObjectsContext *m_ChildContext; + vtkActor *m_BBoxActor; + ::vtkAssembly *m_VtkAsm; // VTK library assembly — NOT this class + bool m_InUpdate; // re-entrancy guard + bool m_BlockUpdate; // block feedback from contentUpdate→Update +}; + +} // namespace Vtk +} // namespace uLib + +#endif // U_VTK_ASSEMBLY_H diff --git a/src/Vtk/Math/vtkContainerBox.h b/src/Vtk/Math/vtkContainerBox.h index 6656306..4b4bfd6 100644 --- a/src/Vtk/Math/vtkContainerBox.h +++ b/src/Vtk/Math/vtkContainerBox.h @@ -51,6 +51,8 @@ public: virtual void Update(); + virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; } + protected: virtual void InstallPipe(); diff --git a/src/Vtk/Math/vtkCylinder.cpp b/src/Vtk/Math/vtkCylinder.cpp index e3e1e8a..d9f2741 100644 --- a/src/Vtk/Math/vtkCylinder.cpp +++ b/src/Vtk/Math/vtkCylinder.cpp @@ -25,6 +25,7 @@ #include "Vtk/Math/vtkCylinder.h" #include +#include #include #include #include @@ -37,13 +38,14 @@ namespace uLib { namespace Vtk { vtkCylinder::vtkCylinder(vtkCylinder::Content *content) - : m_Actor(vtkActor::New()), m_Content(content) { + : m_Content(content), m_Actor(nullptr), m_VtkAsm(nullptr) { this->InstallPipe(); Object::connect(m_Content, &Content::Updated, this, &vtkCylinder::contentUpdate); } vtkCylinder::~vtkCylinder() { - m_Actor->Delete(); + if (m_Actor) m_Actor->Delete(); + if (m_VtkAsm) m_VtkAsm->Delete(); } void vtkCylinder::contentUpdate() { @@ -53,28 +55,31 @@ void vtkCylinder::contentUpdate() { vtkProp3D* root = vtkProp3D::SafeDownCast(this->GetProp()); if (!root) return; + // 1. Placement (Position/Rotation/Model-level Scale) goes to the root prop vtkMatrix4x4* vmat = root->GetUserMatrix(); if (!vmat) { vtkNew mat; root->SetUserMatrix(mat); vmat = mat; } - - // Multiply the placement matrix by the volume scaling (Radius, Radius, Height) - Matrix4f transform = m_Content->GetMatrix() * m_Content->GetLocalMatrix(); + Matrix4f transform = m_Content->GetMatrix(); Matrix4fToVtk(transform, vmat); - // Update internal alignment based on active axis + // 2. Shape-local properties (Radius, Height, Axis alignment) go to the internal actor vtkTransform* alignment = vtkTransform::SafeDownCast(m_Actor->GetUserTransform()); if (alignment) { alignment->Identity(); + alignment->PostMultiply(); + + // Initial source is centered Y-cylinder (Radial XZ [-1,1], Height Y [-0.5, 0.5]) + // Apply Radius and Height scaling + alignment->Scale(m_Content->GetRadius(), m_Content->GetHeight(), m_Content->GetRadius()); + + // Apply Axis alignment int axis = m_Content->GetAxis(); if (axis == 0) alignment->RotateZ(-90); // Y -> X else if (axis == 1) ; // Y -> Y (identity) else if (axis == 2) alignment->RotateX(90); // Y -> Z - - // We keep it centered as per latest user preference in Step 677 - // alignment->Translate(0, 0, 0); // Implicit } root->Modified(); @@ -90,16 +95,9 @@ void vtkCylinder::Update() { vtkMatrix4x4* vmat = root->GetUserMatrix(); if (!vmat) return; - Matrix4f fullTransform = VtkToMatrix4f(vmat); - Matrix4f placementScale = m_Content->GetLocalMatrix().inverse(); - Matrix4f transform = fullTransform * placementScale; - - if (m_Content->GetParent()) { - Matrix4f localT = m_Content->GetParent()->GetWorldMatrix().inverse() * transform; - m_Content->SetMatrix(localT); - } else { - m_Content->SetMatrix(transform); - } + // Pull the placement matrix directly from VTK + Matrix4f transform = VtkToMatrix4f(vmat); + m_Content->SetMatrix(transform); m_Content->Updated(); } @@ -108,36 +106,27 @@ void vtkCylinder::InstallPipe() { if (!m_Content) return; + m_VtkAsm = ::vtkAssembly::New(); + this->SetProp(m_VtkAsm); + vtkNew cylinder; cylinder->SetRadius(1.0); cylinder->SetHeight(1.0); cylinder->SetResolution(32); + m_Actor = vtkActor::New(); vtkNew alignment; - alignment->Identity(); - alignment->Translate(0, 0, -0.5); - - // Default to Y alignment (Identity) as per latest request - int axis = m_Content->GetAxis(); - if (axis == 0) alignment->RotateZ(-90); - else if (axis == 2) alignment->RotateX(90); + m_Actor->SetUserTransform(alignment); vtkNew mapper; mapper->SetInputConnection(cylinder->GetOutputPort()); - m_Actor->SetMapper(mapper); - m_Actor->SetUserTransform(alignment); m_Actor->GetProperty()->SetRepresentationToWireframe(); m_Actor->GetProperty()->SetAmbient(0.6); - this->SetProp(m_Actor); - - vtkProp3D* root = vtkProp3D::SafeDownCast(this->GetProp()); - if (root) { - vtkNew vmat; - Matrix4fToVtk(m_Content->GetMatrix() * m_Content->GetLocalMatrix(), vmat); - root->SetUserMatrix(vmat); - } + m_VtkAsm->AddPart(m_Actor); + + this->contentUpdate(); } } // namespace Vtk diff --git a/src/Vtk/Math/vtkCylinder.h b/src/Vtk/Math/vtkCylinder.h index 20ce7a5..fc246f9 100644 --- a/src/Vtk/Math/vtkCylinder.h +++ b/src/Vtk/Math/vtkCylinder.h @@ -29,6 +29,7 @@ #include "Math/Cylinder.h" #include "Vtk/uLibVtkInterface.h" #include +class vtkAssembly; namespace uLib { namespace Vtk { @@ -53,11 +54,14 @@ public: /** Synchronizes the uLib model matrix with the VTK actor (e.g., after UI manipulation) */ virtual void Update(); + virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; } + protected: /** Sets up the VTK visualization pipeline */ virtual void InstallPipe(); vtkActor *m_Actor; + ::vtkAssembly *m_VtkAsm; Content *m_Content; }; diff --git a/src/Vtk/uLibVtkInterface.h b/src/Vtk/uLibVtkInterface.h index 66cff0e..7110f2f 100644 --- a/src/Vtk/uLibVtkInterface.h +++ b/src/Vtk/uLibVtkInterface.h @@ -61,6 +61,8 @@ public: virtual vtkPropCollection *GetProps(); + virtual uLib::Object *GetContent() const { return nullptr; } + void ConnectRenderer(vtkRenderer *renderer); void DisconnectRenderer(vtkRenderer *renderer); diff --git a/src/Vtk/vtkObjectsContext.cpp b/src/Vtk/vtkObjectsContext.cpp index 133f926..6f157c5 100644 --- a/src/Vtk/vtkObjectsContext.cpp +++ b/src/Vtk/vtkObjectsContext.cpp @@ -1,6 +1,7 @@ #include "vtkObjectsContext.h" #include "Vtk/Math/vtkContainerBox.h" #include "Vtk/Math/vtkCylinder.h" +#include "Vtk/Math/vtkAssembly.h" #include "HEP/Detectors/vtkDetectorChamber.h" #include @@ -42,12 +43,8 @@ void vtkObjectsContext::Synchronize() { it->second->DisconnectRenderer(nullptr); // If we have a ref to a renderer we should disconnect but Puppet doesn't store it easily // Actually Puppet::DisconnectRenderer(vtkRenderer*) needs the renderer. // For now we just remove from assembly - vtkPropCollection* props = it->second->GetProps(); - props->InitTraversal(); - while(vtkProp* prop = props->GetNextProp()) { - if (vtkProp3D* p3d = vtkProp3D::SafeDownCast(prop)) - m_Assembly->RemovePart(p3d); - } + if (auto* p3d = vtkProp3D::SafeDownCast(it->second->GetProp())) + m_Assembly->RemovePart(p3d); this->PuppetRemoved(it->second); delete it->second; it = m_Puppets.erase(it); @@ -62,12 +59,8 @@ void vtkObjectsContext::Synchronize() { Puppet* puppet = this->CreatePuppet(obj); if (puppet) { m_Puppets[obj] = puppet; - vtkPropCollection* props = puppet->GetProps(); - props->InitTraversal(); - while(vtkProp* prop = props->GetNextProp()) { - if (vtkProp3D* p3d = vtkProp3D::SafeDownCast(prop)) - m_Assembly->AddPart(p3d); - } + if (auto* p3d = vtkProp3D::SafeDownCast(puppet->GetProp())) + m_Assembly->AddPart(p3d); this->PuppetAdded(puppet); } } @@ -80,12 +73,8 @@ void vtkObjectsContext::OnObjectAdded(uLib::Object* obj) { Puppet* puppet = this->CreatePuppet(obj); if (puppet) { m_Puppets[obj] = puppet; - vtkPropCollection* props = puppet->GetProps(); - props->InitTraversal(); - while(vtkProp* prop = props->GetNextProp()) { - if (vtkProp3D* p3d = vtkProp3D::SafeDownCast(prop)) - m_Assembly->AddPart(p3d); - } + if (auto* p3d = vtkProp3D::SafeDownCast(puppet->GetProp())) + m_Assembly->AddPart(p3d); this->PuppetAdded(puppet); } } @@ -97,12 +86,8 @@ void vtkObjectsContext::OnObjectRemoved(uLib::Object* obj) { if (it != m_Puppets.end()) { // For now we just remove from assembly. // Puppet::DisconnectRenderer(vtkRenderer*) needs the renderer, but we don't have it here easily. - vtkPropCollection* props = it->second->GetProps(); - props->InitTraversal(); - while(vtkProp* prop = props->GetNextProp()) { - if (vtkProp3D* p3d = vtkProp3D::SafeDownCast(prop)) - m_Assembly->RemovePart(p3d); - } + if (auto* p3d = vtkProp3D::SafeDownCast(it->second->GetProp())) + m_Assembly->RemovePart(p3d); this->PuppetRemoved(it->second); delete it->second; m_Puppets.erase(it); @@ -131,6 +116,8 @@ Puppet* vtkObjectsContext::CreatePuppet(uLib::Object* obj) { return new vtkDetectorChamber(static_cast(obj)); } else if (std::strcmp(className, "Cylinder") == 0) { return new vtkCylinder(static_cast(obj)); + } else if (std::strcmp(className, "Assembly") == 0) { + return new Assembly(static_cast(obj)); } // Fallback if we don't know the exact class but it might be a context itself diff --git a/src/Vtk/vtkObjectsContext.h b/src/Vtk/vtkObjectsContext.h index 0408e47..cfb480a 100644 --- a/src/Vtk/vtkObjectsContext.h +++ b/src/Vtk/vtkObjectsContext.h @@ -25,6 +25,8 @@ public: /** @brief Returns the puppet associated with a specific core object. */ Puppet* GetPuppet(uLib::Object* obj); + const std::map& GetPuppets() const { return m_Puppets; } + /** @brief Updates all managed puppets. */ virtual void Update() override; diff --git a/src/Vtk/vtkViewport.cpp b/src/Vtk/vtkViewport.cpp index 58bed5d..d05522b 100644 --- a/src/Vtk/vtkViewport.cpp +++ b/src/Vtk/vtkViewport.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,12 @@ #include #include #include "vtkHandlerWidget.h" +#include "vtkObjectsContext.h" +#include "Math/Assembly.h" +#include "Math/ContainerBox.h" +#include "Math/Cylinder.h" +#include "Math/Transform.h" +#include "Vtk/Math/vtkAssembly.h" namespace uLib { namespace Vtk { @@ -220,32 +227,67 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren) self->pv->m_Picker->Pick(pos[0], pos[1], 0, self->pv->m_Renderer); vtkProp* picked = self->pv->m_Picker->GetViewProp(); + // 1. Recursive helper to check if a container prop contains a target prop + std::function containsProp; + containsProp = [&containsProp](vtkProp* container, vtkProp* target) -> bool { + if (container == target) return true; + vtkPropCollection* parts = nullptr; + if (auto* pa = vtkPropAssembly::SafeDownCast(container)) + parts = pa->GetParts(); + else if (auto* aa = vtkAssembly::SafeDownCast(container)) + parts = aa->GetParts(); + if (parts) { + parts->InitTraversal(); + for (int i = 0; i < parts->GetNumberOfItems(); ++i) { + if (containsProp(parts->GetNextProp(), target)) + return true; + } + } + return false; + }; + Puppet* target = nullptr; if (picked) { + // 2. Find the leaf puppet: the one that contains 'picked' and is not a parent of another that also contains it. + // Actually, we can just find all matches and pick the one with most 'nested' prop? + // A simpler way: we know 'picked' is the LEAF prop from VTK. + // Find a puppet that contains it. + Puppet* leafPuppet = nullptr; for (auto* p : self->m_Puppets) { - if (p->GetProp() == picked) { - target = p; - break; + if (containsProp(p->GetProp(), picked)) { + // If we already have a candidate, check if this one is smaller (nested) + if (!leafPuppet || containsProp(leafPuppet->GetProp(), p->GetProp())) { + leafPuppet = p; + } } - auto* propAssembly = vtkPropAssembly::SafeDownCast(p->GetProp()); - auto* actorAssembly = vtkAssembly::SafeDownCast(p->GetProp()); - vtkPropCollection* parts = nullptr; - if (propAssembly) parts = propAssembly->GetParts(); - else if (actorAssembly) parts = actorAssembly->GetParts(); + } - if (parts) { - bool found = false; - parts->InitTraversal(); - for (int i=0; iGetNumberOfItems(); ++i) { - if (parts->GetNextProp() == picked) { - found = true; - break; + if (leafPuppet) { + target = leafPuppet; + + // 3. Model-driven hierarchy climb: + // If the leaf puppet has a uLib object, climb its parents. + // If any parent is an Assembly with GroupSelection=true, select the assembly puppet instead. + uLib::Object* currentObj = leafPuppet->GetContent(); + + while (currentObj) { + // Object doesn't have parent, but AffineTransform does + uLib::Object* parentObj = nullptr; + if (auto* at = dynamic_cast(currentObj)) { + parentObj = dynamic_cast(at->GetParent()); + } + + if (auto* parentAsm = dynamic_cast<::uLib::Assembly*>(parentObj)) { + if (parentAsm->GetGroupSelection()) { + // Find the puppet for this parent assembly + auto it = self->m_ObjectToPuppet.find(parentAsm); + if (it != self->m_ObjectToPuppet.end()) { + target = it->second; + // Keep climbing to find even larger groups + } } } - if (found) { - target = p; - break; - } + currentObj = parentObj; } } } @@ -387,20 +429,74 @@ void Viewport::ZoomSelected() void Viewport::AddPuppet(Puppet& prop) { - m_Puppets.push_back(&prop); - prop.ConnectRenderer(pv->m_Renderer); + this->RegisterPuppet(&prop, false); Render(); } void Viewport::RemovePuppet(Puppet& prop) { - if (prop.IsSelected()) SelectPuppet(nullptr); - auto it = std::find(m_Puppets.begin(), m_Puppets.end(), &prop); - if (it != m_Puppets.end()) m_Puppets.erase(it); - prop.DisconnectRenderer(pv->m_Renderer); + this->UnregisterPuppet(&prop); Render(); } +void Viewport::RegisterPuppet(Puppet* p, bool isPart) { + if (!p) return; + if (std::find(m_Puppets.begin(), m_Puppets.end(), p) != m_Puppets.end()) return; + + m_Puppets.push_back(p); + p->ConnectRenderer(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. + if (isPart) { + pv->m_Renderer->RemoveViewProp(p->GetProp()); + } + + // Get the object and register in map + uLib::Object* obj = p->GetContent(); + + // If it's an assembly, we need to observe its children + if (auto* as = dynamic_cast<::uLib::Vtk::Assembly*>(p)) { + this->ObserveContext(as->GetChildrenContext()); + } + + if (obj) m_ObjectToPuppet[obj] = p; +} + +void Viewport::UnregisterPuppet(Puppet* p) { + if (!p) return; + if (p->IsSelected()) SelectPuppet(nullptr); + + auto it = std::find(m_Puppets.begin(), m_Puppets.end(), p); + if (it != m_Puppets.end()) m_Puppets.erase(it); + + // Remove from map + for (auto mapIt = m_ObjectToPuppet.begin(); mapIt != m_ObjectToPuppet.end(); ) { + if (mapIt->second == p) mapIt = m_ObjectToPuppet.erase(mapIt); + else ++mapIt; + } + + p->DisconnectRenderer(pv->m_Renderer); +} + +void Viewport::ObserveContext(vtkObjectsContext* ctx) { + if (!ctx) return; + + // Process existing puppets + for (auto const& [obj, puppet] : ctx->GetPuppets()) { + this->RegisterPuppet(puppet, true); + } + + // Listen for future puppets + uLib::Object::connect(ctx, &vtkObjectsContext::PuppetAdded, [this](Puppet* p){ + this->RegisterPuppet(p, true); + }); + uLib::Object::connect(ctx, &vtkObjectsContext::PuppetRemoved, [this](Puppet* p){ + this->UnregisterPuppet(p); + }); +} + void Viewport::SelectPuppet(Puppet* prop) { for (auto* p : m_Puppets) { diff --git a/src/Vtk/vtkViewport.h b/src/Vtk/vtkViewport.h index 09266e1..173ce3f 100644 --- a/src/Vtk/vtkViewport.h +++ b/src/Vtk/vtkViewport.h @@ -3,6 +3,9 @@ #include "uLibVtkInterface.h" #include +#include + +namespace uLib { class Object; } // VTK classes are in the global namespace class vtkRenderer; @@ -73,10 +76,16 @@ protected: void SetupPipeline(vtkRenderWindowInteractor* iren); void UpdateGrid(); + + // Internal puppet registration + void RegisterPuppet(Puppet* p, bool isPart = false); + void UnregisterPuppet(Puppet* p); + void ObserveContext(class vtkObjectsContext* ctx); struct ViewportData *pv; Axis m_GridAxis; std::vector m_Puppets; + std::map m_ObjectToPuppet; }; } // namespace Vtk