#include "vtkViewport.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 { struct ViewportData { vtkSmartPointer m_Renderer; vtkSmartPointer m_Annotation; vtkSmartPointer m_Marker; vtkSmartPointer m_CameraWidget; vtkSmartPointer m_GridSource; vtkSmartPointer m_GridActor; vtkSmartPointer m_OriginAxes; vtkSmartPointer m_OriginAxesActor; vtkSmartPointer m_Colors; vtkSmartPointer m_HandlerWidget; vtkSmartPointer m_Picker; vtkSmartPointer m_KeyCallback; ViewportData() : m_Renderer(vtkSmartPointer::New()) , m_Annotation(vtkSmartPointer::New()) , m_Marker(vtkSmartPointer::New()) , m_CameraWidget(nullptr) , m_Colors(vtkSmartPointer::New()) {} }; Viewport::Viewport() : pv(new ViewportData()) , m_GridAxis(Y) { } void Viewport::DisableHandler() { if (pv->m_HandlerWidget) { pv->m_HandlerWidget->SetEnabled(0); } } Viewport::~Viewport() { if (pv->m_HandlerWidget) { pv->m_HandlerWidget->SetEnabled(0); pv->m_HandlerWidget->SetInteractor(nullptr); pv->m_HandlerWidget = nullptr; } if (pv->m_Renderer) { if (pv->m_Renderer->GetActiveCamera()) { pv->m_Renderer->GetActiveCamera()->RemoveAllObservers(); } pv->m_Renderer->RemoveAllObservers(); pv->m_Renderer->RemoveAllViewProps(); } if (pv->m_Marker && !std::getenv("CTEST_PROJECT_NAME")) { pv->m_Marker->SetEnabled(false); pv->m_Marker->SetInteractor(nullptr); } if (pv->m_CameraWidget && !std::getenv("CTEST_PROJECT_NAME")) { pv->m_CameraWidget->Off(); pv->m_CameraWidget->SetInteractor(nullptr); } delete pv; } vtkRenderer* Viewport::GetRenderer() { return pv->m_Renderer; } vtkCornerAnnotation* Viewport::GetAnnotation() { return pv->m_Annotation; } vtkCameraOrientationWidget* Viewport::GetCameraWidget() { return pv->m_CameraWidget; } void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren) { if (!iren) return; // Trackball-camera interaction style vtkNew style; iren->SetInteractorStyle(style); // Corner annotation pv->m_Annotation->GetTextProperty()->SetColor(1, 1, 1); pv->m_Annotation->GetTextProperty()->SetFontFamilyToArial(); pv->m_Annotation->GetTextProperty()->SetOpacity(0.5); pv->m_Annotation->SetMaximumFontSize(10); pv->m_Annotation->SetText(0, "uLib VTK viewer."); pv->m_Renderer->AddViewProp(pv->m_Annotation); // right corner annotation pv->m_Annotation->SetText(1, "Grid: -"); // Orientation axes marker (bottom-left corner) if (!std::getenv("CTEST_PROJECT_NAME")) { vtkNew axes; pv->m_Marker->SetInteractor(iren); pv->m_Marker->SetOrientationMarker(axes); pv->m_Marker->SetViewport(0.0, 0.0, 0.2, 0.2); pv->m_Marker->SetEnabled(true); pv->m_Marker->InteractiveOff(); } // Grid Plane centered at (0,0,0) pv->m_GridSource = vtkSmartPointer::New(); pv->m_GridActor = vtkSmartPointer::New(); vtkNew gridMapper; gridMapper->SetInputConnection(pv->m_GridSource->GetOutputPort()); pv->m_GridActor->SetMapper(gridMapper); pv->m_GridActor->GetProperty()->SetRepresentationToWireframe(); pv->m_GridActor->GetProperty()->SetColor(0.4, 0.4, 0.4); pv->m_GridActor->GetProperty()->SetLighting(0); pv->m_GridActor->GetProperty()->SetOpacity(0.5); pv->m_GridActor->PickableOff(); pv->m_Renderer->AddActor(pv->m_GridActor); // Global Origin Axes pv->m_OriginAxes = vtkSmartPointer::New(); pv->m_OriginAxes->SetScaleFactor(1.0); // will be updated vtkNew axesMapper; axesMapper->SetInputConnection(pv->m_OriginAxes->GetOutputPort()); pv->m_OriginAxesActor = vtkSmartPointer::New(); pv->m_OriginAxesActor->SetMapper(axesMapper); pv->m_OriginAxesActor->PickableOff(); pv->m_Renderer->AddActor(pv->m_OriginAxesActor); UpdateGrid(); // Observe interactor to update grid during interaction vtkNew interactionCallback; interactionCallback->SetClientData(this); interactionCallback->SetCallback([](vtkObject*, unsigned long, void* clientdata, void*){ static_cast(clientdata)->UpdateGrid(); }); iren->AddObserver(vtkCommand::InteractionEvent, interactionCallback); pv->m_Renderer->GetActiveCamera()->AddObserver(vtkCommand::ModifiedEvent, interactionCallback); // Camera-orientation widget (VTK >= 9) #if VTK_MAJOR_VERSION >= 9 if (!std::getenv("CTEST_PROJECT_NAME")) { pv->m_CameraWidget = vtkSmartPointer::New(); pv->m_CameraWidget->SetParentRenderer(pv->m_Renderer); pv->m_CameraWidget->SetInteractor(iren); pv->m_CameraWidget->On(); } #endif pv->m_Renderer->SetBackground(0.15, 0.15, 0.15); pv->m_Renderer->ResetCamera(); // Setup layering for overimposed rendering if (iren->GetRenderWindow()) { iren->GetRenderWindow()->SetNumberOfLayers(2); pv->m_Renderer->SetLayer(0); } // Setup Handler Widget if (!std::getenv("CTEST_PROJECT_NAME")) { pv->m_HandlerWidget = vtkSmartPointer::New(); pv->m_HandlerWidget->SetInteractor(iren); pv->m_HandlerWidget->SetCurrentRenderer(pv->m_Renderer); if (pv->m_HandlerWidget->GetOverlayRenderer()) { pv->m_HandlerWidget->GetOverlayRenderer()->SetLayer(1); } // Observe InteractionEvent to update the selected puppet when the widget moves it vtkNew widgetInteractionCallback; widgetInteractionCallback->SetClientData(this); widgetInteractionCallback->SetCallback([](vtkObject*, unsigned long, void* clientdata, void*){ auto* self = static_cast(clientdata); for (auto* p : self->m_Puppets) { if (p->IsSelected()) { p->SyncFromVtk(); } } }); pv->m_HandlerWidget->AddObserver(vtkCommand::InteractionEvent, widgetInteractionCallback); } // Picking for selection pv->m_Picker = vtkSmartPointer::New(); vtkNew clickCallback; clickCallback->SetClientData(this); clickCallback->SetCallback([](vtkObject* caller, unsigned long, void* clientdata, void*){ auto* iren = static_cast(caller); auto* self = static_cast(clientdata); int* pos = iren->GetEventPosition(); 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 (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; } } } 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 } } } currentObj = parentObj; } } } self->SelectPuppet(target); }); iren->AddObserver(vtkCommand::LeftButtonPressEvent, clickCallback); // Keyboard events for widget coordinate frame pv->m_KeyCallback = vtkSmartPointer::New(); pv->m_KeyCallback->SetClientData(this); pv->m_KeyCallback->SetCallback([](vtkObject* caller, unsigned long event, void* clientdata, void*){ auto* iren = static_cast(caller); auto* self = static_cast(clientdata); std::string key = iren->GetKeySym(); bool handled = false; if (self->pv->m_HandlerWidget && self->pv->m_HandlerWidget->GetEnabled()) { if (key == "l") { if (event == vtkCommand::KeyPressEvent) { self->pv->m_HandlerWidget->SetReferenceFrame(vtkHandlerWidget::LOCAL); std::cout << "Widget Frame: LOCAL" << std::endl; } handled = true; } else if (key == "g") { if (event == vtkCommand::KeyPressEvent) { self->pv->m_HandlerWidget->SetReferenceFrame(vtkHandlerWidget::GLOBAL); std::cout << "Widget Frame: GLOBAL" << std::endl; } handled = true; } else if (key == "c") { if (event == vtkCommand::KeyPressEvent) { self->pv->m_HandlerWidget->SetReferenceFrame(vtkHandlerWidget::CENTER); std::cout << "Widget Frame: CENTER" << std::endl; } handled = true; } else if (key == "k") { if (event == vtkCommand::KeyPressEvent) { self->pv->m_HandlerWidget->SetReferenceFrame(vtkHandlerWidget::CENTER_LOCAL); std::cout << "Widget Frame: CENTER_LOCAL" << std::endl; } handled = true; } else if (key == "1") { if (event == vtkCommand::KeyPressEvent) { self->pv->m_HandlerWidget->SetTranslationEnabled(!self->pv->m_HandlerWidget->GetTranslationEnabled()); } handled = true; } else if (key == "2") { if (event == vtkCommand::KeyPressEvent) { self->pv->m_HandlerWidget->SetRotationEnabled(!self->pv->m_HandlerWidget->GetRotationEnabled()); } handled = true; } else if (key == "3") { if (event == vtkCommand::KeyPressEvent) { self->pv->m_HandlerWidget->SetScalingEnabled(!self->pv->m_HandlerWidget->GetScalingEnabled()); } handled = true; } } if (key == "f") { if (event == vtkCommand::KeyPressEvent) { self->ZoomSelected(); } handled = true; } if (handled) { self->pv->m_KeyCallback->SetAbortFlag(1); iren->Render(); } }); iren->AddObserver(vtkCommand::KeyPressEvent, pv->m_KeyCallback, 1.0); iren->AddObserver(vtkCommand::CharEvent, pv->m_KeyCallback, 1.0); } void Viewport::Reset() { ZoomAuto(); Render(); } void Viewport::ZoomAuto() { if (pv->m_Renderer) { pv->m_Renderer->ResetCameraClippingRange(); pv->m_Renderer->ResetCamera(); } } void Viewport::ZoomSelected() { if (!pv->m_Renderer) return; Puppet* selected = nullptr; for (auto* p : m_Puppets) { if (p->IsSelected()) { selected = p; break; } } if (!selected) return; vtkProp* prop = selected->GetProp(); if (!prop) return; double* b = prop->GetBounds(); if (!b) return; double bounds[6]; std::copy(b, b + 6, bounds); if (bounds[0] > bounds[1]) return; // Invalid bounds // Expand bounds by a factor from center (e.g. 2.0 to have some margin) double center[3] = {(bounds[0] + bounds[1]) / 2.0, (bounds[2] + bounds[3]) / 2.0, (bounds[4] + bounds[5]) / 2.0}; double h_ext[3] = {(bounds[1] - bounds[0]) / 2.0, (bounds[3] - bounds[2]) / 2.0, (bounds[5] - bounds[4]) / 2.0}; // Ensure a minimum size to avoid camera issues with flat/point objects double max_h = std::max({h_ext[0], h_ext[1], h_ext[2], 0.1}); double newBounds[6]; for (int i=0; i<3; ++i) { double current_h = std::max(h_ext[i], max_h * 0.1); newBounds[2*i] = center[i] - 2.5 * current_h; newBounds[2*i+1] = center[i] + 2.5 * current_h; } pv->m_Renderer->ResetCamera(newBounds); pv->m_Renderer->ResetCameraClippingRange(); this->Render(); } void Viewport::AddPuppet(Puppet& prop) { this->RegisterPuppet(&prop, false); Render(); } void Viewport::RemovePuppet(Puppet& prop) { 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) { p->SetSelected(p == prop); } if (pv->m_HandlerWidget) { if (prop) { vtkProp3D* prop3d = prop->GetProxyProp(); if (prop3d) { pv->m_HandlerWidget->SetProp3D(prop3d); pv->m_HandlerWidget->SetEnabled(1); pv->m_HandlerWidget->PlaceWidget(prop3d->GetBounds()); //TODO: FIX ! } } else { pv->m_HandlerWidget->SetEnabled(0); pv->m_HandlerWidget->SetProp3D(nullptr); } } Render(); OnSelectionChanged(prop); } void Viewport::SetGridVisible(bool visible) { if (pv->m_GridActor) { pv->m_GridActor->SetVisibility(visible); Render(); } } bool Viewport::GetGridVisible() const { if (pv->m_GridActor) { return pv->m_GridActor->GetVisibility() != 0; } return false; } void Viewport::SetGridAxis(Axis axis) { m_GridAxis = axis; UpdateGrid(); Render(); } void Viewport::addProp(vtkProp* prop) { if (pv->m_Renderer) { pv->m_Renderer->AddActor(prop); Render(); } } void Viewport::RemoveProp(vtkProp* prop) { if (pv->m_Renderer) { pv->m_Renderer->RemoveViewProp(prop); Render(); } } void Viewport::UpdateGrid() { if (!pv->m_Renderer || !pv->m_GridSource) return; if (pv->m_GridActor && !pv->m_GridActor->GetVisibility()) return; vtkCamera* camera = pv->m_Renderer->GetActiveCamera(); if (!camera) return; // Determine the "scale" of the view (how many units are visible vertically) double viewHeight; if (camera->GetParallelProjection()) { viewHeight = 2.0 * camera->GetParallelScale(); } else { double distance = camera->GetDistance(); // ViewAngle is height angle in degrees double angleRad = camera->GetViewAngle() * vtkMath::Pi() / 180.0; viewHeight = 2.0 * distance * std::tan(angleRad / 2.0); } if (viewHeight <= 0) viewHeight = 1.0; // We want roughly 5-15 grid divisions visible. // Spacing should be a power of 10 (1mm, 1cm, 10cm, 1m, 10m...) double log10Spacing = std::floor(std::log10(viewHeight / 5.0)); double spacing = std::pow(10.0, log10Spacing); // Spacing should be at least 1mm if (spacing < 1.0) spacing = 1.0; // Get current focal point to center the grid near what we're looking at double focalPoint[3]; camera->GetFocalPoint(focalPoint); // Indices for the two dimensions of the grid plane int idxH, idxV, idxN; if (m_GridAxis == X) { idxH = 1; idxV = 2; idxN = 0; } else if (m_GridAxis == Y) { idxH = 0; idxV = 2; idxN = 1; } else { idxH = 0; idxV = 1; idxN = 2; } // Align center to spacing double centerH = std::round(focalPoint[idxH] / spacing) * spacing; double centerV = std::round(focalPoint[idxV] / spacing) * spacing; double centerN = 0.0; // Grid plane typically passes through the origin // Number of lines int numLines = 20; double halfSize = (numLines / 2.0) * spacing; double minH = centerH - halfSize; double maxH = centerH + halfSize; double minV = centerV - halfSize; double maxV = centerV + halfSize; // Update Plane Source mapping axes to origin/point1/point2 double origin[3] = {0,0,0}, p1[3] = {0,0,0}, p2[3] = {0,0,0}; origin[idxH] = minH; origin[idxV] = minV; origin[idxN] = centerN; p1[idxH] = maxH; p1[idxV] = minV; p1[idxN] = centerN; p2[idxH] = minH; p2[idxV] = maxV; p2[idxN] = centerN; pv->m_GridSource->SetOrigin(origin); pv->m_GridSource->SetPoint1(p1); pv->m_GridSource->SetPoint2(p2); pv->m_GridSource->SetXResolution(numLines); pv->m_GridSource->SetYResolution(numLines); pv->m_GridSource->Update(); if (pv->m_OriginAxes) { pv->m_OriginAxes->SetScaleFactor(spacing); } // Update annotation for grid size char gridLabel[32]; if (spacing >= 1000.0) { sprintf(gridLabel, "Grid: %.1f m", spacing / 1000.0); } else if (spacing >= 10.0) { sprintf(gridLabel, "Grid: %.1f cm", spacing / 10.0); } else { sprintf(gridLabel, "Grid: %.0f mm", spacing); } pv->m_Annotation->SetText(1, gridLabel); } } // namespace Vtk } // namespace uLib