From 46c39bc26e0e5420108abca491cbdf19192edb39 Mon Sep 17 00:00:00 2001 From: AndreaRigoni Date: Fri, 27 Mar 2026 16:55:26 +0000 Subject: [PATCH] add assembly to gcompose, not working yet --- app/gcompose/src/ContextModel.cpp | 140 ++++++++++++++++++++---- app/gcompose/src/ContextModel.h | 7 ++ app/gcompose/src/ContextPanel.cpp | 4 + src/Core/Object.h | 4 + src/Core/ObjectsContext.h | 1 + src/Math/Assembly.cpp | 37 +++++-- src/Math/Assembly.h | 9 +- src/Math/ContainerBox.h | 2 +- src/Math/Cylinder.h | 2 +- src/Math/MathRegistrations.cpp | 2 + src/Math/Transform.h | 2 +- src/Vtk/Math/vtkAssembly.cpp | 1 + src/Vtk/Math/vtkAssembly.h | 1 + src/Vtk/testing/CMakeLists.txt | 1 + src/Vtk/testing/PuppetParentingTest.cpp | 106 ++++++++++++++++++ 15 files changed, 287 insertions(+), 32 deletions(-) create mode 100644 src/Vtk/testing/PuppetParentingTest.cpp diff --git a/app/gcompose/src/ContextModel.cpp b/app/gcompose/src/ContextModel.cpp index dd1d42c..a8933ea 100644 --- a/app/gcompose/src/ContextModel.cpp +++ b/app/gcompose/src/ContextModel.cpp @@ -4,6 +4,11 @@ #include #include #include "Core/Object.h" +#include +#include +#include +#include +#include ContextModel::ContextModel(QObject* parent) : QAbstractItemModel(parent), m_rootContext(nullptr) {} @@ -11,12 +16,16 @@ ContextModel::ContextModel(QObject* parent) ContextModel::~ContextModel() {} void ContextModel::setContext(uLib::ObjectsContext* context) { + m_isReseting = true; beginResetModel(); m_rootContext = context; if (m_rootContext) { auto refresh = [this]() { + if (this->m_isReseting) return; + this->m_isReseting = true; this->beginResetModel(); this->endResetModel(); + this->m_isReseting = false; }; uLib::Object::connect(m_rootContext, &uLib::Object::Updated, refresh); @@ -25,7 +34,6 @@ void ContextModel::setContext(uLib::ObjectsContext* context) { refresh(); }); uLib::Object::connect(m_rootContext, &uLib::ObjectsContext::ObjectRemoved, [this, refresh](uLib::Object* obj) { - // Disconnect would be good here but not strictly required if refresh handles it refresh(); }); @@ -35,6 +43,7 @@ void ContextModel::setContext(uLib::ObjectsContext* context) { } } endResetModel(); + m_isReseting = false; } QModelIndex ContextModel::index(int row, int column, const QModelIndex& parent) const { @@ -48,8 +57,8 @@ QModelIndex ContextModel::index(int row, int column, const QModelIndex& parent) } } else { uLib::Object* parentObj = static_cast(parent.internalPointer()); - uLib::ObjectsContext* parentCtx = dynamic_cast(parentObj); - if (parentCtx && row < parentCtx->GetCount()) { + uLib::ObjectsContext* parentCtx = parentObj->GetChildren(); + if (parentCtx && row < (int)parentCtx->GetCount()) { return createIndex(row, column, parentCtx->GetObject(row)); } } @@ -65,36 +74,37 @@ QModelIndex ContextModel::parent(const QModelIndex& child) const { // Finding the parent of childObj is O(N) since there is no parent pointer. // We just do a recursive search starting from root context. - std::function findParent = - [&findParent](uLib::ObjectsContext* ctx, uLib::Object* target) -> uLib::ObjectsContext* { - for (const auto& obj : ctx->GetObjects()) { - if (obj == target) return ctx; - if (auto subCtx = dynamic_cast(obj)) { - if (auto p = findParent(subCtx, target)) return p; + std::function findParent = + [&findParent](uLib::Object* current, uLib::Object* target) -> uLib::Object* { + uLib::ObjectsContext* ctx = current->GetChildren(); + if (ctx) { + for (const auto& obj : ctx->GetObjects()) { + if (obj == target) return current; + if (auto p = findParent(obj, target)) return p; } } return nullptr; }; - uLib::ObjectsContext* parentCtx = findParent(m_rootContext, childObj); - if (!parentCtx || parentCtx == m_rootContext) { + uLib::Object* parentObj = findParent(m_rootContext, childObj); + if (!parentObj || parentObj == m_rootContext) { return QModelIndex(); // Root items have invalid parent index } - // Now need to find the row of parentCtx in its own parent Context. - uLib::ObjectsContext* grandParentCtx = findParent(m_rootContext, parentCtx); - if (!grandParentCtx) grandParentCtx = m_rootContext; + // Now need to find the row of parentObj in its own parent Context. + uLib::Object* grandParentObj = findParent(m_rootContext, parentObj); + uLib::ObjectsContext* grandParentCtx = grandParentObj ? grandParentObj->GetChildren() : m_rootContext; int row = -1; for (size_t i = 0; i < grandParentCtx->GetCount(); ++i) { - if (grandParentCtx->GetObject(i) == parentCtx) { + if (grandParentCtx->GetObject(i) == parentObj) { row = (int)i; break; } } if (row != -1) { - return createIndex(row, 0, parentCtx); + return createIndex(row, 0, parentObj); } return QModelIndex(); } @@ -107,8 +117,8 @@ int ContextModel::rowCount(const QModelIndex& parent) const { } uLib::Object* parentObj = static_cast(parent.internalPointer()); - if (auto parentCtx = dynamic_cast(parentObj)) { - return parentCtx->GetCount(); + if (auto parentCtx = parentObj->GetChildren()) { + return (int)parentCtx->GetCount(); } return 0; // leaf node } @@ -161,8 +171,98 @@ QVariant ContextModel::headerData(int section, Qt::Orientation orientation, int } Qt::ItemFlags ContextModel::flags(const QModelIndex& index) const { - if (!index.isValid()) return Qt::NoItemFlags; - return Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled; + if (!index.isValid()) return m_rootContext ? Qt::ItemIsDropEnabled : Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled; + uLib::Object* obj = static_cast(index.internalPointer()); + if (dynamic_cast(obj)) { + f |= Qt::ItemIsDropEnabled; + } + return f; +} + +Qt::DropActions ContextModel::supportedDropActions() const { + return Qt::MoveAction; +} + +QStringList ContextModel::mimeTypes() const { + return {"application/x-ulib-object-ptr"}; +} + +QMimeData* ContextModel::mimeData(const QModelIndexList& indexes) const { + QMimeData* mimeData = new QMimeData(); + QByteArray encodedData; + QDataStream stream(&encodedData, QIODevice::WriteOnly); + for (const auto& idx : indexes) { + if (idx.isValid() && idx.column() == 0) { + void* ptr = idx.internalPointer(); + stream << reinterpret_cast(ptr); + } + } + mimeData->setData("application/x-ulib-object-ptr", encodedData); + return mimeData; +} + +bool ContextModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) { + if (action != Qt::MoveAction || !data->hasFormat("application/x-ulib-object-ptr")) return false; + + uLib::ObjectsContext* targetCtx = m_rootContext; + if (parent.isValid()) { + uLib::Object* parentObj = static_cast(parent.internalPointer()); + targetCtx = dynamic_cast(parentObj); + } + if (!targetCtx) return false; + + QByteArray encodedData = data->data("application/x-ulib-object-ptr"); + QDataStream stream(&encodedData, QIODevice::ReadOnly); + std::vector objectsToMove; + while (!stream.atEnd()) { + qlonglong ptrVal; + stream >> ptrVal; + objectsToMove.push_back(reinterpret_cast(ptrVal)); + } + + if (objectsToMove.empty()) return false; + + // Helper to find and remove from current parent + std::function findAndRemoveRecursive = + [&findAndRemoveRecursive](uLib::Object* current, uLib::Object* target) { + if (auto ctx = current->GetChildren()) { + ctx->RemoveObject(target); + for (auto* obj : ctx->GetObjects()) { + findAndRemoveRecursive(obj, target); + } + } + }; + + m_isReseting = true; + beginResetModel(); + for (auto* obj : objectsToMove) { + // Don't drop onto itself or its descendants + bool invalid = (obj == targetCtx || obj == (uLib::Object*)targetCtx); + if (!invalid) { + // check if targetCtx is descendant of obj + std::function isDescendant = + [&isDescendant](uLib::Object* root, uLib::Object* target) -> bool { + if (auto ctx = root->GetChildren()) { + for (auto* child : ctx->GetObjects()) { + if (child == target) return true; + if (isDescendant(child, target)) return true; + } + } + return false; + }; + if (isDescendant(obj, (uLib::Object*)targetCtx)) invalid = true; + } + + if (!invalid) { + findAndRemoveRecursive(m_rootContext, obj); + targetCtx->AddObject(obj); + } + } + endResetModel(); + m_isReseting = false; + return true; } bool ContextModel::setData(const QModelIndex& index, const QVariant& value, int role) { diff --git a/app/gcompose/src/ContextModel.h b/app/gcompose/src/ContextModel.h index de6d5ea..778e6de 100644 --- a/app/gcompose/src/ContextModel.h +++ b/app/gcompose/src/ContextModel.h @@ -21,8 +21,15 @@ public: Qt::ItemFlags flags(const QModelIndex& index) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + // Drag and Drop support + Qt::DropActions supportedDropActions() const override; + QStringList mimeTypes() const override; + QMimeData* mimeData(const QModelIndexList& indexes) const override; + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + private: uLib::ObjectsContext* m_rootContext; + bool m_isReseting = false; }; #endif // CONTEXT_MODEL_H diff --git a/app/gcompose/src/ContextPanel.cpp b/app/gcompose/src/ContextPanel.cpp index 24b7567..f34a986 100644 --- a/app/gcompose/src/ContextPanel.cpp +++ b/app/gcompose/src/ContextPanel.cpp @@ -38,6 +38,10 @@ ContextPanel::ContextPanel(QWidget* parent) m_treeView = new QTreeView(this); m_treeView->setObjectName("ContextTree"); m_treeView->setHeaderHidden(false); + m_treeView->setDragEnabled(true); + m_treeView->setAcceptDrops(true); + m_treeView->setDropIndicatorShown(true); + m_treeView->setDragDropMode(QAbstractItemView::DragDrop); m_model = new ContextModel(this); m_treeView->setModel(m_model); diff --git a/src/Core/Object.h b/src/Core/Object.h index 9eb31e1..4b0fda9 100644 --- a/src/Core/Object.h +++ b/src/Core/Object.h @@ -52,6 +52,7 @@ class polymorphic_oarchive; namespace uLib { class PropertyBase; +class ObjectsContext; class Version { public: @@ -101,6 +102,9 @@ public: // FIXX !!! virtual void DeepCopy(const Object ©); + /** @brief Returns a nested context for children objects, if any. */ + virtual ObjectsContext* GetChildren() { return nullptr; } + //////////////////////////////////////////////////////////////////////////// // SERIALIZATION // diff --git a/src/Core/ObjectsContext.h b/src/Core/ObjectsContext.h index f555dd2..57b213a 100644 --- a/src/Core/ObjectsContext.h +++ b/src/Core/ObjectsContext.h @@ -15,6 +15,7 @@ public: virtual ~ObjectsContext(); virtual const char * GetClassName() const { return "ObjectsContext"; } + virtual ObjectsContext* GetChildren() override { return this; } /** * @brief Adds an object to the context. diff --git a/src/Math/Assembly.cpp b/src/Math/Assembly.cpp index 1966fdd..ed24f2b 100644 --- a/src/Math/Assembly.cpp +++ b/src/Math/Assembly.cpp @@ -25,7 +25,9 @@ Assembly::Assembly() m_BBoxMin(Vector3f::Zero()), m_BBoxMax(Vector3f::Zero()), m_ShowBoundingBox(false), - m_GroupSelection(true) {} + m_GroupSelection(true) { + ULIB_ACTIVATE_PROPERTIES(*this); +} Assembly::Assembly(const Assembly ©) : ObjectsContext(copy), @@ -35,13 +37,25 @@ Assembly::Assembly(const Assembly ©) m_ShowBoundingBox(copy.m_ShowBoundingBox), m_GroupSelection(copy.m_GroupSelection) {} -Assembly::~Assembly() {} +Assembly::~Assembly() { + for (auto const& [obj, conn] : m_ChildConnections) { + conn.disconnect(); + } + m_ChildConnections.clear(); +} void Assembly::AddObject(Object *obj) { if (auto *at = dynamic_cast(obj)) { at->SetParent(this); } ObjectsContext::AddObject(obj); + + // Connect to child updates to recompute AABB + m_ChildConnections[obj] = Object::connect(obj, &Object::Updated, [this](){ + this->ComputeBoundingBox(); + this->Updated(); // Signal that assembly itself changed (AABB-wise) + }); + this->ComputeBoundingBox(); } void Assembly::RemoveObject(Object *obj) { @@ -49,7 +63,15 @@ void Assembly::RemoveObject(Object *obj) { if (at->GetParent() == this) at->SetParent(nullptr); } + + auto itConn = m_ChildConnections.find(obj); + if (itConn != m_ChildConnections.end()) { + itConn->second.disconnect(); + m_ChildConnections.erase(itConn); + } + ObjectsContext::RemoveObject(obj); + this->ComputeBoundingBox(); } void Assembly::ComputeBoundingBox() { @@ -64,12 +86,11 @@ void Assembly::ComputeBoundingBox() { 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(); + // ContainerBox: wm is matrix from unit cube [0,1] to local space + // Since it is parented to 'this', GetMatrix() is sufficient. + Matrix4f m = box->GetMatrix(); for (int i = 0; i < 8; ++i) { float x = (i & 1) ? 1.0f : 0.0f; float y = (i & 2) ? 1.0f : 0.0f; @@ -82,7 +103,7 @@ void Assembly::ComputeBoundingBox() { } } else if (auto *cyl = dynamic_cast(obj)) { // Cylinder: centered [-1, 1] radial, [-0.5, 0.5] height - Matrix4f m = invAsm * cyl->GetWorldMatrix(); + Matrix4f m = cyl->GetMatrix(); for (int i = 0; i < 8; ++i) { float x = (i & 1) ? 1.0f : -1.0f; float y = (i & 2) ? 0.5f : -0.5f; @@ -98,7 +119,7 @@ void Assembly::ComputeBoundingBox() { subAsm->ComputeBoundingBox(); Vector3f subMin, subMax; subAsm->GetBoundingBox(subMin, subMax); - Matrix4f m = invAsm * subAsm->GetWorldMatrix(); + Matrix4f m = subAsm->GetMatrix(); for (int i = 0; i < 8; ++i) { float x = (i & 1) ? subMax(0) : subMin(0); float y = (i & 2) ? subMax(1) : subMin(1); diff --git a/src/Math/Assembly.h b/src/Math/Assembly.h index 9ee9242..a96105c 100644 --- a/src/Math/Assembly.h +++ b/src/Math/Assembly.h @@ -52,6 +52,12 @@ public: Assembly(const Assembly ©); virtual ~Assembly(); + template + void serialize(ArchiveT & ar, const unsigned int version) { + ar & boost::serialization::make_nvp("AffineTransform", boost::serialization::base_object(*this)); + ar & boost::serialization::make_hrp("GroupSelection", m_GroupSelection); + } + virtual void AddObject(Object* obj) override; virtual void RemoveObject(Object* obj) override; @@ -93,7 +99,7 @@ signals: if (m_InUpdated) return; // break signal recursion m_InUpdated = true; this->ComputeBoundingBox(); - ULIB_SIGNAL_EMIT(Assembly::Updated); + ULIB_SIGNAL_EMIT(Object::Updated); m_InUpdated = false; } @@ -103,6 +109,7 @@ private: bool m_ShowBoundingBox; bool m_GroupSelection; bool m_InUpdated = false; + std::map m_ChildConnections; }; } // namespace uLib diff --git a/src/Math/ContainerBox.h b/src/Math/ContainerBox.h index 2a2f926..0e724cf 100644 --- a/src/Math/ContainerBox.h +++ b/src/Math/ContainerBox.h @@ -215,7 +215,7 @@ signals: /** Signal emitted when properties change */ virtual void Updated() override { this->Sync(); - ULIB_SIGNAL_EMIT(ContainerBox::Updated); + ULIB_SIGNAL_EMIT(Object::Updated); } private: diff --git a/src/Math/Cylinder.h b/src/Math/Cylinder.h index a9b33ce..7ba88fa 100644 --- a/src/Math/Cylinder.h +++ b/src/Math/Cylinder.h @@ -177,7 +177,7 @@ signals: /** Signal emitted when properties change */ virtual void Updated() override { this->Sync(); - ULIB_SIGNAL_EMIT(Cylinder::Updated); + ULIB_SIGNAL_EMIT(Object::Updated); } private: diff --git a/src/Math/MathRegistrations.cpp b/src/Math/MathRegistrations.cpp index 445552a..53d0903 100644 --- a/src/Math/MathRegistrations.cpp +++ b/src/Math/MathRegistrations.cpp @@ -5,12 +5,14 @@ #include "Math/TriangleMesh.h" #include "Math/QuadMesh.h" #include "Math/VoxImage.h" +#include "Math/Assembly.h" #include "Math/StructuredData.h" namespace uLib { ULIB_REGISTER_OBJECT(ContainerBox) ULIB_REGISTER_OBJECT(Cylinder) +ULIB_REGISTER_OBJECT(Assembly) ULIB_REGISTER_OBJECT(CylindricalGeometry) ULIB_REGISTER_OBJECT(SphericalGeometry) ULIB_REGISTER_OBJECT(TriangleMesh) diff --git a/src/Math/Transform.h b/src/Math/Transform.h index dd4a42f..bf92041 100644 --- a/src/Math/Transform.h +++ b/src/Math/Transform.h @@ -285,7 +285,7 @@ signals: /** Signal emitted when properties change */ virtual void Updated() override { this->Sync(); - ULIB_SIGNAL_EMIT(AffineTransform::Updated); + ULIB_SIGNAL_EMIT(Object::Updated); } private: diff --git a/src/Vtk/Math/vtkAssembly.cpp b/src/Vtk/Math/vtkAssembly.cpp index d25941d..55c7b71 100644 --- a/src/Vtk/Math/vtkAssembly.cpp +++ b/src/Vtk/Math/vtkAssembly.cpp @@ -70,6 +70,7 @@ void Assembly::InstallPipe() { 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->PickableOff(); m_BBoxActor->SetVisibility(m_Content ? m_Content->GetShowBoundingBox() : false); m_VtkAsm->AddPart(m_BBoxActor); diff --git a/src/Vtk/Math/vtkAssembly.h b/src/Vtk/Math/vtkAssembly.h index a63d54f..17defa9 100644 --- a/src/Vtk/Math/vtkAssembly.h +++ b/src/Vtk/Math/vtkAssembly.h @@ -48,6 +48,7 @@ public: virtual void SyncFromVtk() override; virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; } + virtual uLib::ObjectsContext* GetChildren() override { return (uLib::ObjectsContext*)m_Content; } /** @brief Called when the model signals an update (model→VTK push). */ void contentUpdate(); diff --git a/src/Vtk/testing/CMakeLists.txt b/src/Vtk/testing/CMakeLists.txt index bf15373..df717ea 100644 --- a/src/Vtk/testing/CMakeLists.txt +++ b/src/Vtk/testing/CMakeLists.txt @@ -3,6 +3,7 @@ set(TESTS vtkViewerTest vtkHandlerWidget PuppetPropertyTest + PuppetParentingTest # vtkVoxImageTest # vtkTriangleMeshTest ) diff --git a/src/Vtk/testing/PuppetParentingTest.cpp b/src/Vtk/testing/PuppetParentingTest.cpp new file mode 100644 index 0000000..b9136df --- /dev/null +++ b/src/Vtk/testing/PuppetParentingTest.cpp @@ -0,0 +1,106 @@ +/*////////////////////////////////////////////////////////////////////////////// +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "testing-prototype.h" + +using namespace uLib; + +int main() { + BEGIN_TESTING(Puppet Parenting Test); + + ObjectsContext globalContext; + Vtk::Viewer viewer; + + // Create the display context, linked to the model context. + // It will automatically create visual puppets for each model object. + Vtk::vtkObjectsContext viewerContext(&globalContext); + viewerContext.ConnectRenderer(viewer.GetRenderer()); + + // 1. Create a model Assembly + auto* assembly = new Assembly(); + assembly->SetInstanceName("ParentAssembly"); + globalContext.AddObject(assembly); + + // Verify assembly puppet exists in the viewer context + Vtk::Puppet* assemblyPuppet = viewerContext.GetPuppet(assembly); + ASSERT_NOT_NULL(assemblyPuppet); + + // cast to Vtk::Assembly to access child context + auto* vtkAss = dynamic_cast(assemblyPuppet); + ASSERT_NOT_NULL(vtkAss); + + // 2. Create a child Box and add it to the Assembly + auto* box1 = new ContainerBox(Vector3f(10, 10, 10)); + box1->SetInstanceName("ChildBox1"); + box1->SetPosition(Vector3f(20, 0, 0)); + assembly->AddObject(box1); + + // Verify child puppet was created in the assembly's child context + Vtk::vtkObjectsContext* childVtkCtx = vtkAss->GetChildrenContext(); + ASSERT_NOT_NULL(childVtkCtx); + + Vtk::Puppet* box1Puppet = childVtkCtx->GetPuppet(box1); + ASSERT_NOT_NULL(box1Puppet); + + // 3. Move the parent and verify the child follows + assembly->SetPosition(Vector3f(100, 0, 0)); + assembly->Update(); + + // In VTK assemblies, the child's absolute matrix should reflect the parent's transform + vtkProp3D* box1Prop = vtkProp3D::SafeDownCast(box1Puppet->GetProp()); + ASSERT_NOT_NULL(box1Prop); + + vtkMatrix4x4* boxMatrix = box1Prop->GetMatrix(); + // Origin (0,0,0) + local(20,0,0) + assembly(100,0,0) = world(120,0,0) ? + // Actually, box1->GetPosition() is (20,0,0). + // The puppet ApplyTransform sets the prop orientation and position. + + std::cout << "Checking transformation chain..." << std::endl; + // std::cout << *boxMatrix << std::endl; + + // Verify relative positioning + double* pos = box1Prop->GetPosition(); + ASSERT_EQUAL(pos[0], 20.0); + + // The absolute world position can be checked via GetMatrix elements + // boxMatrix->GetElement(0, 3) should be 120.0 if the vtkAssembly nesting is working + // but vtkAssembly::GetMatrix() usually returns the LOCAL matrix unless called on the top property context? + // Actually vtkProp3D::GetMatrix() is the local matrix. + + // 4. Add another child + auto* box2 = new ContainerBox(Vector3f(5, 5, 5)); + box2->SetInstanceName("ChildBox2"); + box2->SetPosition(Vector3f(-20, 0, 0)); + assembly->AddObject(box2); + + Vtk::Puppet* box2Puppet = childVtkCtx->GetPuppet(box2); + ASSERT_NOT_NULL(box2Puppet); + + // Render if not in batch environment + if (!std::getenv("CTEST_PROJECT_NAME")) { + viewer.GetRenderer()->ResetCamera(); + viewer.Start(); + } + + END_TESTING; +}