diff --git a/src/Vtk/testing/CMakeLists.txt b/src/Vtk/testing/CMakeLists.txt index e5c9e48..ab8b4e0 100644 --- a/src/Vtk/testing/CMakeLists.txt +++ b/src/Vtk/testing/CMakeLists.txt @@ -2,6 +2,7 @@ set( TESTS vtkViewerTest vtkContainerBoxTest + vtkHandlerWidget # vtkVoxImageTest # vtkTriangleMeshTest ) diff --git a/src/Vtk/testing/Makefile.am b/src/Vtk/testing/Makefile.am deleted file mode 100644 index 4cc3180..0000000 --- a/src/Vtk/testing/Makefile.am +++ /dev/null @@ -1,21 +0,0 @@ - -include $(top_srcdir)/Common.am -include ../Vtk.am - -#AM_DEFAULT_SOURCE_EXT = .cpp - -# if HAVE_CHECK -TESTS = \ - vtkViewerTest \ - vtkContainerBoxTest \ - vtkMuonScatter \ - vtkStructuredGridTest \ - vtkVoxRaytracerTest \ - vtkVoxImageTest -# vtkTriangleMeshTest - -all: $(TESTS) - -LDADD = $(top_srcdir)/libmutom-${PACKAGE_VERSION}.la $(AM_LIBS_ALL) -check_PROGRAMS = $(TESTS) - diff --git a/src/Vtk/testing/vtkHandlerWidget.cpp b/src/Vtk/testing/vtkHandlerWidget.cpp new file mode 100644 index 0000000..fa269a4 --- /dev/null +++ b/src/Vtk/testing/vtkHandlerWidget.cpp @@ -0,0 +1,116 @@ +/*////////////////////////////////////////////////////////////////////////////// +// 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. + +//////////////////////////////////////////////////////////////////////////////*/ + +#include "Math/ContainerBox.h" +#include "Vtk/uLibVtkViewer.h" +#include "Vtk/vtkContainerBox.h" +#include "Vtk/vtkHandlerWidget.h" +#include "testing-prototype.h" + +#include +#include +#include +#include +#include +#include + +using namespace uLib; + +int main() { + BEGIN_TESTING(vtkHandlerWidget with ContainerBox); + + // 1. Create a ContainerBox (Math object) + ContainerBox box; + box.Scale(Vector3f(2.0, 3.0, 4.0)); + box.SetPosition(Vector3f(1.0, 1.0, 1.0)); + + // 2. Wrap it in a Vtk::vtkContainerBox (Vtk Puppet) + Vtk::vtkContainerBox v_box(&box); + v_box.SetRepresentation(Vtk::Puppet::Surface); + v_box.SetOpacity(0.5); + + // 3. Setup the Viewer + Vtk::Viewer viewer; + viewer.AddPuppet(v_box); + + // 4. Create and setup the vtkHandlerWidget + vtkSmartPointer handler = + vtkSmartPointer::New(); + + handler->SetInteractor(viewer.GetInteractor()); + + // Get the prop from the puppet and cast it to vtkProp3D + vtkProp *v_prop = v_box.GetProp(); + vtkProp3D *prop = vtkProp3D::SafeDownCast(v_prop); + if (!prop) { + // If it's a PropAssembly, try to get the first part (the cube) + ::vtkPropAssembly *assembly = ::vtkPropAssembly::SafeDownCast(v_prop); + if (assembly && assembly->GetParts()->GetNumberOfItems() > 0) { + assembly->GetParts()->InitTraversal(); + prop = vtkProp3D::SafeDownCast(assembly->GetParts()->GetNextProp()); + } + } + TEST1(prop != nullptr); + + handler->SetProp3D(prop); + handler->PlaceWidget(); + handler->EnabledOn(); + + // 5. Add a key callback to switch modes + auto key_callback = vtkSmartPointer::New(); + key_callback->SetCallback([](vtkObject *caller, unsigned long, void *clientData, void *) { + auto interactor = static_cast(caller); + auto h = static_cast(clientData); + std::string key = interactor->GetKeySym(); + if (key == "g") { + std::cout << "Switching to GLOBAL frame" << std::endl; + h->SetReferenceFrame(Vtk::vtkHandlerWidget::GLOBAL); + } else if (key == "l") { + std::cout << "Switching to LOCAL frame" << std::endl; + h->SetReferenceFrame(Vtk::vtkHandlerWidget::LOCAL); + } else if (key == "c") { + std::cout << "Switching to CENTER frame" << std::endl; + h->SetReferenceFrame(Vtk::vtkHandlerWidget::CENTER); + } else if (key == "k") { + std::cout << "Switching to CENTER_LOCAL frame" << std::endl; + h->SetReferenceFrame(Vtk::vtkHandlerWidget::CENTER_LOCAL); + } + }); + key_callback->SetClientData(handler.GetPointer()); + viewer.GetInteractor()->AddObserver(vtkCommand::CharEvent, key_callback); + + // 6. Start interaction if not in a continuous integration environment + if (std::getenv("CTEST_PROJECT_NAME") == nullptr) { + std::cout << "Interactive test: use the gizmos to transform the ContainerBox" + << std::endl; + std::cout << "Keys: [g] GLOBAL, [l] LOCAL, [c] CENTER, [k] CENTER_LOCAL" << std::endl; + viewer.Start(); + } else { + std::cout << "Non-interactive test: widget initialized successfully" + << std::endl; + } + + END_TESTING; +} diff --git a/src/Vtk/vtkHandlerWidget.cpp b/src/Vtk/vtkHandlerWidget.cpp index e039565..3bf8802 100644 --- a/src/Vtk/vtkHandlerWidget.cpp +++ b/src/Vtk/vtkHandlerWidget.cpp @@ -29,16 +29,19 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -52,10 +55,19 @@ vtkStandardNewMacro(vtkHandlerWidget); vtkHandlerWidget::vtkHandlerWidget() { this->Interaction = IDLE; - this->m_Picker = vtkSmartPointer<::vtkPropPicker>::New(); + this->m_Picker = vtkSmartPointer<::vtkCellPicker>::New(); + this->m_Picker->SetTolerance(0.01); // Increased tolerance for thin gizmos this->m_InitialTransform = vtkSmartPointer<::vtkTransform>::New(); this->EventCallbackCommand->SetCallback(vtkHandlerWidget::ProcessEvents); this->EventCallbackCommand->SetClientData(this); + this->m_Frame = LOCAL; + this->m_HighlightedProp = nullptr; + this->m_ClipPlane = vtkSmartPointer<::vtkPlane>::New(); + this->m_OverlayRenderer = vtkSmartPointer<::vtkRenderer>::New(); + this->m_OverlayRenderer->SetLayer(1); + this->m_OverlayRenderer->EraseOff(); + this->m_OverlayRenderer->InteractiveOff(); + this->Priority = 50.0; // Higher priority to beat camera style this->CreateGizmos(); } @@ -85,23 +97,39 @@ void vtkHandlerWidget::SetEnabled(int enabling) { // Add observers ::vtkRenderWindowInteractor *i = this->Interactor; + this->SetPriority(this->Priority); i->AddObserver(::vtkCommand::LeftButtonPressEvent, this->EventCallbackCommand, this->Priority); i->AddObserver(::vtkCommand::LeftButtonReleaseEvent, this->EventCallbackCommand, this->Priority); i->AddObserver(::vtkCommand::MouseMoveEvent, this->EventCallbackCommand, this->Priority); + i->AddObserver(::vtkCommand::RenderEvent, this->EventCallbackCommand, + this->Priority); this->UpdateGizmoPosition(); - this->CurrentRenderer->AddActor(m_AxesX); - this->CurrentRenderer->AddActor(m_AxesY); - this->CurrentRenderer->AddActor(m_AxesZ); - this->CurrentRenderer->AddActor(m_RotX); - this->CurrentRenderer->AddActor(m_RotY); - this->CurrentRenderer->AddActor(m_RotZ); - this->CurrentRenderer->AddActor(m_ScaleX); - this->CurrentRenderer->AddActor(m_ScaleY); - this->CurrentRenderer->AddActor(m_ScaleZ); + + // Manage Layers + auto win = this->Interactor->GetRenderWindow(); + if (win->GetNumberOfLayers() < 2) { + win->SetNumberOfLayers(2); + } + + // Sync Viewport and Camera + this->m_OverlayRenderer->SetViewport(this->CurrentRenderer->GetViewport()); + this->m_OverlayRenderer->SetActiveCamera(this->CurrentRenderer->GetActiveCamera()); + win->AddRenderer(this->m_OverlayRenderer); + + this->m_OverlayRenderer->AddActor(m_AxesX); + this->m_OverlayRenderer->AddActor(m_AxesY); + this->m_OverlayRenderer->AddActor(m_AxesZ); + this->m_OverlayRenderer->AddActor(m_RotX); + this->m_OverlayRenderer->AddActor(m_RotY); + this->m_OverlayRenderer->AddActor(m_RotZ); + this->m_OverlayRenderer->AddActor(m_RotCam); + this->m_OverlayRenderer->AddActor(m_ScaleX); + this->m_OverlayRenderer->AddActor(m_ScaleY); + this->m_OverlayRenderer->AddActor(m_ScaleZ); this->InvokeEvent(::vtkCommand::EnableEvent, nullptr); } else { @@ -109,18 +137,12 @@ void vtkHandlerWidget::SetEnabled(int enabling) { return; this->Enabled = 0; + this->Highlight(nullptr); this->Interactor->RemoveObserver(this->EventCallbackCommand); - if (this->CurrentRenderer) { - this->CurrentRenderer->RemoveActor(m_AxesX); - this->CurrentRenderer->RemoveActor(m_AxesY); - this->CurrentRenderer->RemoveActor(m_AxesZ); - this->CurrentRenderer->RemoveActor(m_RotX); - this->CurrentRenderer->RemoveActor(m_RotY); - this->CurrentRenderer->RemoveActor(m_RotZ); - this->CurrentRenderer->RemoveActor(m_ScaleX); - this->CurrentRenderer->RemoveActor(m_ScaleY); - this->CurrentRenderer->RemoveActor(m_ScaleZ); + if (this->Interactor->GetRenderWindow()) { + this->Interactor->GetRenderWindow()->RemoveRenderer(this->m_OverlayRenderer); } + this->m_OverlayRenderer->RemoveAllViewProps(); this->InvokeEvent(::vtkCommand::DisableEvent, nullptr); } @@ -137,12 +159,22 @@ void vtkHandlerWidget::ProcessEvents(::vtkObject *caller, unsigned long event, switch (event) { case ::vtkCommand::LeftButtonPressEvent: self->OnLeftButtonDown(); + if (self->Interaction != ::uLib::Vtk::vtkHandlerWidget::IDLE) + self->EventCallbackCommand->SetAbortFlag(1); break; case ::vtkCommand::LeftButtonReleaseEvent: self->OnLeftButtonUp(); + if (self->EventCallbackCommand->GetAbortFlag()) { + // Don't let release bleed if press was captured + } break; case ::vtkCommand::MouseMoveEvent: self->OnMouseMove(); + if (self->Interaction != ::uLib::Vtk::vtkHandlerWidget::IDLE) + self->EventCallbackCommand->SetAbortFlag(1); + break; + case ::vtkCommand::RenderEvent: + self->UpdateGizmoPosition(); break; } } @@ -155,8 +187,9 @@ void vtkHandlerWidget::OnLeftButtonDown() { this->CurrentRenderer = this->Interactor->FindPokedRenderer(X, Y); } - this->m_Picker->Pick(X, Y, 0.0, this->CurrentRenderer); + this->m_Picker->Pick(X, Y, 0.0, this->m_OverlayRenderer); ::vtkProp *prop = this->m_Picker->GetViewProp(); + this->m_Picker->GetPickPosition(this->m_StartPickPosition); if (!prop) return; @@ -180,6 +213,8 @@ void vtkHandlerWidget::OnLeftButtonDown() { this->Interaction = SCALE_Y; else if (prop == m_ScaleZ) this->Interaction = SCALE_Z; + else if (prop == m_RotCam) + this->Interaction = ROT_CAM; if (this->Interaction != IDLE) { this->StartEventPosition[0] = X; @@ -198,62 +233,186 @@ void vtkHandlerWidget::OnLeftButtonUp() { return; this->Interaction = IDLE; + this->EventCallbackCommand->SetAbortFlag(1); this->InvokeEvent(::vtkCommand::EndInteractionEvent, nullptr); this->Interactor->Render(); } void vtkHandlerWidget::OnMouseMove() { - if (this->Interaction == IDLE || !this->Prop3D) + if (!this->Prop3D || !this->CurrentRenderer) return; int X = this->Interactor->GetEventPosition()[0]; int Y = this->Interactor->GetEventPosition()[1]; + if (this->Interaction == IDLE) { + this->m_Picker->Pick(X, Y, 0.0, this->m_OverlayRenderer); + ::vtkProp *prop = this->m_Picker->GetViewProp(); + this->Highlight(prop); + this->UpdateGizmoPosition(); // Ensure camera adjustments happen + return; + } + double dx = X - this->StartEventPosition[0]; double dy = Y - this->StartEventPosition[1]; - vtkSmartPointer<::vtkTransform> t = vtkSmartPointer<::vtkTransform>::New(); - t->PostMultiply(); - t->SetMatrix(this->m_InitialTransform->GetMatrix()); + // Get current gizmo properties from its actors + vtkMatrix4x4 *gizmo_mat = m_AxesX->GetUserMatrix(); + if (!gizmo_mat) + return; - double factor = 0.01; + double gpos[3] = {gizmo_mat->GetElement(0, 3), gizmo_mat->GetElement(1, 3), + gizmo_mat->GetElement(2, 3)}; + + // Normalized gizmo axes + double gx[3] = {gizmo_mat->GetElement(0, 0), gizmo_mat->GetElement(1, 0), + gizmo_mat->GetElement(2, 0)}; + double gy[3] = {gizmo_mat->GetElement(0, 1), gizmo_mat->GetElement(1, 1), + gizmo_mat->GetElement(2, 1)}; + double gz[3] = {gizmo_mat->GetElement(0, 2), gizmo_mat->GetElement(1, 2), + gizmo_mat->GetElement(2, 2)}; + + auto get_motion_magnitude = [&](double axis[3], double origin[3]) { + double p1[3] = {origin[0], origin[1], origin[2]}; + double p2[3] = {origin[0] + axis[0], origin[1] + axis[1], + origin[2] + axis[2]}; + + double d1[3], d2[3]; + this->ComputeWorldToDisplay(this->CurrentRenderer, p1[0], p1[1], p1[2], d1); + this->ComputeWorldToDisplay(this->CurrentRenderer, p2[0], p2[1], p2[2], d2); + + double v[2] = {d2[0] - d1[0], d2[1] - d1[1]}; + double v_mag_sq = v[0] * v[0] + v[1] * v[1]; + if (v_mag_sq < 1.0) + return 0.0; + return (dx * v[0] + dy * v[1]) / v_mag_sq; + }; + + auto get_rotation_magnitude = [&](double axis[3]) { + // Tangent at pick point + double v_pick[3] = {this->m_StartPickPosition[0] - gpos[0], + this->m_StartPickPosition[1] - gpos[1], + this->m_StartPickPosition[2] - gpos[2]}; + double tangent[3]; + vtkMath::Cross(axis, v_pick, tangent); + vtkMath::Normalize(tangent); + + double p1[3] = {this->m_StartPickPosition[0], this->m_StartPickPosition[1], this->m_StartPickPosition[2]}; + double p2[3] = {p1[0] + tangent[0], p1[1] + tangent[1], p1[2] + tangent[2]}; + + double d1[3], d2[3]; + this->ComputeWorldToDisplay(this->CurrentRenderer, p1[0], p1[1], p1[2], d1); + this->ComputeWorldToDisplay(this->CurrentRenderer, p2[0], p2[1], p2[2], d2); + + double v[2] = {d2[0] - d1[0], d2[1] - d1[1]}; + double v_mag_sq = v[0] * v[0] + v[1] * v[1]; + if (v_mag_sq < 1.0) return 0.0; + + // Return pixels along tangent (mapped to degrees) + return (dx * v[0] + dy * v[1]) / sqrt(v_mag_sq); + }; + + // Create a transform that represents the operation in Gizmo-local space + vtkNew delta; + delta->PostMultiply(); + delta->Translate(-gpos[0], -gpos[1], -gpos[2]); + + // Orientation of the gizmo + vtkNew orient; + orient->Identity(); + for (int i = 0; i < 3; ++i) { + orient->SetElement(i, 0, gx[i]); + orient->SetElement(i, 1, gy[i]); + orient->SetElement(i, 2, gz[i]); + } + vtkNew orient_inv; + vtkMatrix4x4::Invert(orient, orient_inv); + delta->Concatenate(orient_inv); + + // Now the coordinate system is at gizmo center, aligned with its axes. + double mag = 0; switch (this->Interaction) { case TRANS_X: - t->Translate(dx * factor, 0, 0); + mag = get_motion_magnitude(gx, gpos); + delta->Translate(mag, 0, 0); break; case TRANS_Y: - t->Translate(0, dy * factor, 0); + mag = get_motion_magnitude(gy, gpos); + delta->Translate(0, mag, 0); break; case TRANS_Z: - t->Translate(0, 0, dy * factor); + mag = get_motion_magnitude(gz, gpos); + delta->Translate(0, 0, mag); break; case ROT_X: - t->RotateX(dy); + mag = get_rotation_magnitude(gx); + delta->RotateX(mag); break; case ROT_Y: - t->RotateY(dx); + mag = get_rotation_magnitude(gy); + delta->RotateY(mag); break; case ROT_Z: - t->RotateZ(dx); + mag = get_rotation_magnitude(gz); + delta->RotateZ(mag); break; case SCALE_X: - t->Scale(std::max(0.1, 1.0 + dx * factor), 1.0, 1.0); + mag = get_motion_magnitude(gx, gpos); + delta->Scale(std::max(0.1, 1.0 + mag), 1.0, 1.0); break; case SCALE_Y: - t->Scale(1.0, std::max(0.1, 1.0 + dy * factor), 1.0); + mag = get_motion_magnitude(gy, gpos); + delta->Scale(1.0, std::max(0.1, 1.0 + mag), 1.0); break; case SCALE_Z: - t->Scale(1.0, 1.0, std::max(0.1, 1.0 + dy * factor)); + mag = get_motion_magnitude(gz, gpos); + delta->Scale(1.0, 1.0, std::max(0.1, 1.0 + mag)); + break; + case ROT_CAM: { + // Rotate around camera-viewer axis + double camPos[3]; + this->CurrentRenderer->GetActiveCamera()->GetPosition(camPos); + double dir[3] = {camPos[0] - gpos[0], camPos[1] - gpos[1], + camPos[2] - gpos[2]}; + vtkMath::Normalize(dir); + + // Orientation of the gizmo is currently orient + // But delta is in gizmo-local. + // In gizmo-local, the camera direction is: + double dir4[4] = {dir[0], dir[1], dir[2], 0.0}; + double dir_local4[4]; + orient_inv->MultiplyPoint(dir4, dir_local4); + double axis_local[3] = {dir_local4[0], dir_local4[1], dir_local4[2]}; + mag = get_rotation_magnitude(dir); // Tangent calculated in world space + delta->RotateWXYZ(mag, axis_local[0], axis_local[1], axis_local[2]); break; } + } - this->Prop3D->SetUserMatrix(t->GetMatrix()); + // Back to world space + delta->Concatenate(orient); + delta->Translate(gpos[0], gpos[1], gpos[2]); + + // Apply delta on top of the initial object state + vtkNew final_t; + final_t->PostMultiply(); + final_t->SetMatrix(this->m_InitialTransform->GetMatrix()); + final_t->Concatenate(delta); + + this->Prop3D->SetUserMatrix(final_t->GetMatrix()); this->UpdateGizmoPosition(); this->InvokeEvent(::vtkCommand::InteractionEvent, nullptr); this->Interactor->Render(); } +void vtkHandlerWidget::SetReferenceFrame(ReferenceFrame frame) { + this->m_Frame = frame; + this->UpdateGizmoPosition(); + if (this->Interactor) + this->Interactor->Render(); +} + void vtkHandlerWidget::PlaceWidget(double bounds[6]) { (void)bounds; this->UpdateGizmoPosition(); @@ -277,47 +436,50 @@ void vtkHandlerWidget::GetTransform(::vtkTransform *t) { void vtkHandlerWidget::CreateGizmos() { auto create_arrow = [](double dir[3], double color[3]) { auto arrow = vtkSmartPointer<::vtkArrowSource>::New(); + arrow->SetTipLength(0.2); + arrow->SetTipRadius(0.06); + arrow->SetShaftRadius(0.015); + auto mapper = vtkSmartPointer<::vtkPolyDataMapper>::New(); mapper->SetInputConnection(arrow->GetOutputPort()); auto actor = vtkSmartPointer<::vtkActor>::New(); actor->SetMapper(mapper); actor->GetProperty()->SetColor(color); + actor->GetProperty()->SetLighting(0); // Saturated colors, no shadows - auto t = vtkSmartPointer<::vtkTransform>::New(); if (dir[1] > 0) - t->RotateZ(90); + actor->RotateZ(90); else if (dir[2] > 0) - t->RotateY(-90); - actor->SetUserTransform(t); + actor->RotateY(-90); return actor; }; - auto create_ring = [](int axis, double color[3]) { - auto arc = vtkSmartPointer<::vtkArcSource>::New(); - arc->SetCenter(0, 0, 0); - arc->SetResolution(64); - if (axis == 0) { - arc->SetPoint1(0, 1, 0); - arc->SetPoint2(0, -1, 0); - } else if (axis == 1) { - arc->SetPoint1(1, 0, 0); - arc->SetPoint2(-1, 0, 0); - } else if (axis == 2) { - arc->SetPoint1(1, 0, 0); - arc->SetPoint2(-1, 0, 0); - } + auto create_ring = [&](int axis, double color[3]) { + auto circle = vtkSmartPointer<::vtkRegularPolygonSource>::New(); + circle->SetNumberOfSides(64); + circle->SetRadius(1.0); + circle->SetCenter(0, 0, 0); + circle->GeneratePolygonOff(); + circle->GeneratePolylineOn(); + + if (axis == 0) circle->SetNormal(1, 0, 0); + else if (axis == 1) circle->SetNormal(0, 1, 0); + else if (axis == 2) circle->SetNormal(0, 0, 1); auto mapper = vtkSmartPointer<::vtkPolyDataMapper>::New(); - mapper->SetInputConnection(arc->GetOutputPort()); + mapper->SetInputConnection(circle->GetOutputPort()); + mapper->AddClippingPlane(this->m_ClipPlane); + auto actor = vtkSmartPointer<::vtkActor>::New(); actor->SetMapper(mapper); actor->GetProperty()->SetColor(color); actor->GetProperty()->SetLineWidth(3); + actor->GetProperty()->SetLighting(0); return actor; }; - double red[] = {0.8, 0.1, 0.1}, green[] = {0.1, 0.8, 0.1}, - blue[] = {0.1, 0.1, 0.8}; + double red[] = {1.0, 0.0, 0.0}, green[] = {0.0, 1.0, 0.0}, + blue[] = {0.0, 0.0, 1.0}, white[] = {1.0, 1.0, 1.0}; double x[] = {1, 0, 0}, y[] = {0, 1, 0}, z[] = {0, 0, 1}; m_AxesX = create_arrow(x, red); @@ -328,17 +490,34 @@ void vtkHandlerWidget::CreateGizmos() { m_RotY = create_ring(1, green); m_RotZ = create_ring(2, blue); + m_RotCam = vtkSmartPointer<::vtkActor>::New(); + { + auto circle = vtkSmartPointer<::vtkRegularPolygonSource>::New(); + circle->SetNumberOfSides(64); + circle->SetRadius(1.3); // Slightly larger + circle->SetCenter(0, 0, 0); + circle->GeneratePolygonOff(); + circle->GeneratePolylineOn(); + auto mapper = vtkSmartPointer<::vtkPolyDataMapper>::New(); + mapper->SetInputConnection(circle->GetOutputPort()); + m_RotCam->SetMapper(mapper); + m_RotCam->GetProperty()->SetColor(white); + m_RotCam->GetProperty()->SetLineWidth(2); + m_RotCam->GetProperty()->SetLighting(0); + } + auto create_cube = [](double pos[3], double color[3]) { auto cube = vtkSmartPointer<::vtkCubeSource>::New(); cube->SetCenter(pos[0], pos[1], pos[2]); - cube->SetXLength(0.12); - cube->SetYLength(0.12); - cube->SetZLength(0.12); + cube->SetXLength(0.08); + cube->SetYLength(0.08); + cube->SetZLength(0.08); auto mapper = vtkSmartPointer<::vtkPolyDataMapper>::New(); mapper->SetInputConnection(cube->GetOutputPort()); auto actor = vtkSmartPointer<::vtkActor>::New(); actor->SetMapper(mapper); actor->GetProperty()->SetColor(color); + actor->GetProperty()->SetLighting(0); return actor; }; @@ -346,21 +525,173 @@ void vtkHandlerWidget::CreateGizmos() { m_ScaleX = create_cube(px, red); m_ScaleY = create_cube(py, green); m_ScaleZ = create_cube(pz, blue); + + // Configure picker to only see gizmo actors (Pick-Through) + m_Picker->InitializePickList(); + m_Picker->AddPickList(m_AxesX); + m_Picker->AddPickList(m_AxesY); + m_Picker->AddPickList(m_AxesZ); + m_Picker->AddPickList(m_RotX); + m_Picker->AddPickList(m_RotY); + m_Picker->AddPickList(m_RotZ); + m_Picker->AddPickList(m_RotCam); + m_Picker->AddPickList(m_ScaleX); + m_Picker->AddPickList(m_ScaleY); + m_Picker->AddPickList(m_ScaleZ); + m_Picker->PickFromListOn(); } void vtkHandlerWidget::UpdateGizmoPosition() { if (!this->Prop3D) return; - ::vtkMatrix4x4 *mat = this->Prop3D->GetMatrix(); - m_AxesX->SetUserMatrix(mat); - m_AxesY->SetUserMatrix(mat); - m_AxesZ->SetUserMatrix(mat); - m_RotX->SetUserMatrix(mat); - m_RotY->SetUserMatrix(mat); - m_RotZ->SetUserMatrix(mat); - m_ScaleX->SetUserMatrix(mat); - m_ScaleY->SetUserMatrix(mat); - m_ScaleZ->SetUserMatrix(mat); + + vtkNew mat_gizmo; + mat_gizmo->Identity(); + + double center[3]; + double bounds[6]; + this->Prop3D->GetBounds(bounds); + center[0] = (bounds[0] + bounds[1]) / 2.0; + center[1] = (bounds[2] + bounds[3]) / 2.0; + center[2] = (bounds[4] + bounds[5]) / 2.0; + + double pos[3]; + this->Prop3D->GetPosition(pos); + + if (m_Frame == LOCAL) { + ::vtkMatrix4x4 *mat = this->Prop3D->GetMatrix(); + mat_gizmo->DeepCopy(mat); + // Remove scaling + for (int j = 0; j < 3; ++j) { + double v[3] = {mat->GetElement(0, j), mat->GetElement(1, j), + mat->GetElement(2, j)}; + double len = vtkMath::Norm(v); + if (len > 1e-6) { + mat_gizmo->SetElement(0, j, v[0] / len); + mat_gizmo->SetElement(1, j, v[1] / len); + mat_gizmo->SetElement(2, j, v[2] / len); + } + } + } else if (m_Frame == CENTER_LOCAL) { + ::vtkMatrix4x4 *mat = this->Prop3D->GetMatrix(); + mat_gizmo->DeepCopy(mat); + // Remove scaling + for (int j = 0; j < 3; ++j) { + double v[3] = {mat->GetElement(0, j), mat->GetElement(1, j), + mat->GetElement(2, j)}; + double len = vtkMath::Norm(v); + if (len > 1e-6) { + mat_gizmo->SetElement(0, j, v[0] / len); + mat_gizmo->SetElement(1, j, v[1] / len); + mat_gizmo->SetElement(2, j, v[2] / len); + } + } + // Set position to center + mat_gizmo->SetElement(0, 3, center[0]); + mat_gizmo->SetElement(1, 3, center[1]); + mat_gizmo->SetElement(2, 3, center[2]); + } else if (m_Frame == GLOBAL) { + mat_gizmo->Identity(); + mat_gizmo->SetElement(0, 3, pos[0]); + mat_gizmo->SetElement(1, 3, pos[1]); + mat_gizmo->SetElement(2, 3, pos[2]); + } else if (m_Frame == CENTER) { + mat_gizmo->Identity(); + mat_gizmo->SetElement(0, 3, center[0]); + mat_gizmo->SetElement(1, 3, center[1]); + mat_gizmo->SetElement(2, 3, center[2]); + } + + m_AxesX->SetUserMatrix(mat_gizmo); + m_AxesY->SetUserMatrix(mat_gizmo); + m_AxesZ->SetUserMatrix(mat_gizmo); + m_RotX->SetUserMatrix(mat_gizmo); + m_RotY->SetUserMatrix(mat_gizmo); + m_RotZ->SetUserMatrix(mat_gizmo); + m_ScaleX->SetUserMatrix(mat_gizmo); + m_ScaleY->SetUserMatrix(mat_gizmo); + m_ScaleZ->SetUserMatrix(mat_gizmo); + + // Sync Overlay Renderer with Main Renderer + if (this->CurrentRenderer && this->m_OverlayRenderer) { + this->m_OverlayRenderer->SetViewport(this->CurrentRenderer->GetViewport()); + this->m_OverlayRenderer->SetAspect(this->CurrentRenderer->GetAspect()); + this->m_OverlayRenderer->ComputeAspect(); + + if (this->m_OverlayRenderer->GetActiveCamera() != this->CurrentRenderer->GetActiveCamera()) { + this->m_OverlayRenderer->SetActiveCamera(this->CurrentRenderer->GetActiveCamera()); + } + } + + // Camera ring always faces camera + if (this->CurrentRenderer) { + double camPos[3]; + this->CurrentRenderer->GetActiveCamera()->GetPosition(camPos); + double dir[3] = {camPos[0] - mat_gizmo->GetElement(0, 3), + camPos[1] - mat_gizmo->GetElement(1, 3), + camPos[2] - mat_gizmo->GetElement(2, 3)}; + vtkMath::Normalize(dir); + + // Orient RotCam actor to face 'dir' + vtkNew tcam; + tcam->PostMultiply(); + // Default circle is in XY plane (Normal Z: 0,0,1) + double z[3] = {0, 0, 1}; + double cross[3]; + vtkMath::Cross(z, dir, cross); + double cross_mag = vtkMath::Norm(cross); + if (cross_mag > 1e-6) { + double angle = vtkMath::DegreesFromRadians(acos(vtkMath::Dot(z, dir))); + tcam->RotateWXYZ(angle, cross[0], cross[1], cross[2]); + } else if (vtkMath::Dot(z, dir) < 0) { + tcam->RotateX(180); + } + tcam->Translate(mat_gizmo->GetElement(0, 3), mat_gizmo->GetElement(1, 3), + mat_gizmo->GetElement(2, 3)); + m_RotCam->SetUserMatrix(tcam->GetMatrix()); + + // Update clipping plane for axes rings + this->m_ClipPlane->SetOrigin(mat_gizmo->GetElement(0, 3), + mat_gizmo->GetElement(1, 3), + mat_gizmo->GetElement(2, 3)); + this->m_ClipPlane->SetNormal(dir); + } +} + +void vtkHandlerWidget::Highlight(::vtkProp *prop) { + if (this->m_HighlightedProp == prop) + return; + + // Restore previous + if (this->m_HighlightedProp) { + ::vtkActor *actor = ::vtkActor::SafeDownCast(this->m_HighlightedProp); + if (actor) { + actor->GetProperty()->SetColor(m_OriginalColor); + actor->GetProperty()->SetLineWidth(3); + } + } + + this->m_HighlightedProp = nullptr; + + // Highlight new if it belongs to us + if (prop == m_AxesX || prop == m_AxesY || prop == m_AxesZ || prop == m_RotX || + prop == m_RotY || prop == m_RotZ || prop == m_RotCam || prop == m_ScaleX || + prop == m_ScaleY || prop == m_ScaleZ) { + this->m_HighlightedProp = prop; + ::vtkActor *actor = ::vtkActor::SafeDownCast(prop); + if (actor) { + actor->GetProperty()->GetColor(m_OriginalColor); + double h[3] = {m_OriginalColor[0] + 0.2, m_OriginalColor[1] + 0.2, + m_OriginalColor[2] + 0.2}; + for (int i = 0; i < 3; ++i) + h[i] = std::min(1.0, h[i]); + actor->GetProperty()->SetColor(h); + actor->GetProperty()->SetLineWidth(5); + } + } + + if (this->Interactor) + this->Interactor->Render(); } } // namespace Vtk diff --git a/src/Vtk/vtkHandlerWidget.h b/src/Vtk/vtkHandlerWidget.h index 7c048c6..6bc1a28 100644 --- a/src/Vtk/vtkHandlerWidget.h +++ b/src/Vtk/vtkHandlerWidget.h @@ -35,9 +35,10 @@ // Forward declarations of VTK classes in global namespace class vtkActor; class vtkCallbackCommand; -class vtkPropPicker; +class vtkCellPicker; class vtkTransform; class vtkObject; +class vtkPlane; class vtkRenderWindowInteractor; namespace uLib { @@ -73,9 +74,22 @@ public: ROT_Z, SCALE_X, SCALE_Y, - SCALE_Z + SCALE_Z, + ROT_CAM }; + enum ReferenceFrame { + GLOBAL = 0, + LOCAL, + CENTER, + CENTER_LOCAL, + NORMAL, // Not implemented + PARENT // Not implemented + }; + + void SetReferenceFrame(ReferenceFrame frame); + ReferenceFrame GetReferenceFrame() const { return this->m_Frame; } + using ::vtk3DWidget::PlaceWidget; virtual void PlaceWidget(double bounds[6]) override; virtual void PlaceWidget() override; @@ -87,17 +101,27 @@ public: protected: void CreateGizmos(); void UpdateGizmoPosition(); + void Highlight(::vtkProp *prop); + + vtkSmartPointer<::vtkRenderer> m_OverlayRenderer; + ReferenceFrame m_Frame; int Interaction; + ::vtkProp *m_HighlightedProp; + double m_OriginalColor[3]; // Visual components // vtkSmartPointer<::vtkActor> m_AxesX, m_AxesY, m_AxesZ; // Arrows vtkSmartPointer<::vtkActor> m_RotX, m_RotY, m_RotZ; // Rings + vtkSmartPointer<::vtkActor> m_RotCam; // Camera ring vtkSmartPointer<::vtkActor> m_ScaleX, m_ScaleY, m_ScaleZ; // Cubes - vtkSmartPointer<::vtkPropPicker> m_Picker; + vtkSmartPointer<::vtkPlane> m_ClipPlane; + + vtkSmartPointer<::vtkCellPicker> m_Picker; double StartEventPosition[2]; + double m_StartPickPosition[3]; vtkSmartPointer<::vtkTransform> m_InitialTransform; private: