/*////////////////////////////////////////////////////////////////////////////// // 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 "vtkHandlerWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace uLib { namespace Vtk { vtkStandardNewMacro(vtkHandlerWidget); vtkHandlerWidget::vtkHandlerWidget() { this->Interaction = IDLE; 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(); } vtkHandlerWidget::~vtkHandlerWidget() {} void vtkHandlerWidget::SetEnabled(int enabling) { if (enabling) { if (this->Enabled) return; if (!this->Interactor) { vtkErrorMacro( << "The interactor must be set prior to enabling the widget"); return; } if (!this->CurrentRenderer) { this->CurrentRenderer = this->Interactor->FindPokedRenderer( this->Interactor->GetLastEventPosition()[0], this->Interactor->GetLastEventPosition()[1]); } if (!this->CurrentRenderer) { return; } this->Enabled = 1; // 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(); // 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 { if (!this->Enabled) return; this->Enabled = 0; this->Highlight(nullptr); this->Interactor->RemoveObserver(this->EventCallbackCommand); if (this->Interactor->GetRenderWindow()) { this->Interactor->GetRenderWindow()->RemoveRenderer(this->m_OverlayRenderer); } this->m_OverlayRenderer->RemoveAllViewProps(); this->InvokeEvent(::vtkCommand::DisableEvent, nullptr); } if (this->Interactor) { this->Interactor->Render(); } } void vtkHandlerWidget::ProcessEvents(::vtkObject *caller, unsigned long event, void *clientdata, void *calldata) { (void)caller; (void)calldata; vtkHandlerWidget *self = reinterpret_cast(clientdata); 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; } } void vtkHandlerWidget::OnLeftButtonDown() { int X = this->Interactor->GetEventPosition()[0]; int Y = this->Interactor->GetEventPosition()[1]; if (!this->CurrentRenderer) { this->CurrentRenderer = this->Interactor->FindPokedRenderer(X, Y); } 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; this->Interaction = IDLE; if (prop == m_AxesX) this->Interaction = TRANS_X; else if (prop == m_AxesY) this->Interaction = TRANS_Y; else if (prop == m_AxesZ) this->Interaction = TRANS_Z; else if (prop == m_RotX) this->Interaction = ROT_X; else if (prop == m_RotY) this->Interaction = ROT_Y; else if (prop == m_RotZ) this->Interaction = ROT_Z; else if (prop == m_ScaleX) this->Interaction = SCALE_X; else if (prop == m_ScaleY) 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; this->StartEventPosition[1] = Y; if (this->Prop3D) { if (!this->Prop3D->GetUserMatrix()) { vtkNew vmat; this->Prop3D->SetUserMatrix(vmat); } this->m_InitialTransform->SetMatrix(this->Prop3D->GetUserMatrix()); } this->EventCallbackCommand->SetAbortFlag(1); this->InvokeEvent(::vtkCommand::StartInteractionEvent, nullptr); this->Interactor->Render(); } } void vtkHandlerWidget::OnLeftButtonUp() { if (this->Interaction == IDLE) return; this->Interaction = IDLE; this->EventCallbackCommand->SetAbortFlag(1); this->InvokeEvent(::vtkCommand::EndInteractionEvent, nullptr); this->Interactor->Render(); } void vtkHandlerWidget::OnMouseMove() { 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]; if (dx == 0 && dy == 0) return; std::cout << "Interaction " << this->Interaction << " dx=" << dx << " dy=" << dy << std::endl; // Get current gizmo properties from its actors vtkMatrix4x4 *gizmo_mat = m_AxesX->GetUserMatrix(); if (!gizmo_mat) return; 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]) -> double { // 1. Get Mouse Ray in World Space double worldNear[4], worldFar[4]; this->ComputeDisplayToWorld(static_cast(X), static_cast(Y), 0.0, worldNear); this->ComputeDisplayToWorld(static_cast(X), static_cast(Y), 1.0, worldFar); double camPos[3]; this->CurrentRenderer->GetActiveCamera()->GetPosition(camPos); double rayDir[3]; for(int i=0; i<3; ++i) rayDir[i] = worldFar[i] - worldNear[i]; vtkMath::Normalize(rayDir); // 2. Intersect Ray with Rotation Plane (Center = gpos, Normal = axis) double planeNormal[3] = {axis[0], axis[1], axis[2]}; vtkMath::Normalize(planeNormal); double denom = vtkMath::Dot(rayDir, planeNormal); if (std::abs(denom) < 1e-6) return 0.0; double t = (vtkMath::Dot(gpos, planeNormal) - vtkMath::Dot(camPos, planeNormal)) / denom; double p_current[3]; for(int i=0; i<3; ++i) p_current[i] = camPos[i] + t * rayDir[i]; // 3. Calculate Angular Displacement in Plane double v_start[3] = {this->m_StartPickPosition[0] - gpos[0], this->m_StartPickPosition[1] - gpos[1], this->m_StartPickPosition[2] - gpos[2]}; double v_end[3] = {p_current[0] - gpos[0], p_current[1] - gpos[1], p_current[2] - gpos[2]}; double r_start = vtkMath::Norm(v_start); double r_end = vtkMath::Norm(v_end); if (r_start < 1e-9 || r_end < 1e-9) return 0.0; for(int i=0; i<3; ++i) { v_start[i] /= r_start; v_end[i] /= r_end; } double cross[3]; vtkMath::Cross(v_start, v_end, cross); double sinAng = vtkMath::Dot(cross, planeNormal); double cosAng = vtkMath::Dot(v_start, v_end); return vtkMath::DegreesFromRadians(atan2(sinAng, cosAng)); }; // Create a transform that represents the operation in world space around gpos vtkNew op; op->PostMultiply(); op->Translate(-gpos[0], -gpos[1], -gpos[2]); double mag = 0; switch (this->Interaction) { case TRANS_X: mag = get_motion_magnitude(gx, gpos); op->Translate(mag * gx[0], mag * gx[1], mag * gx[2]); break; case TRANS_Y: mag = get_motion_magnitude(gy, gpos); op->Translate(mag * gy[0], mag * gy[1], mag * gy[2]); break; case TRANS_Z: mag = get_motion_magnitude(gz, gpos); op->Translate(mag * gz[0], mag * gz[1], mag * gz[2]); break; case ROT_X: mag = get_rotation_magnitude(gx); { double ax[3] = {gx[0], gx[1], gx[2]}; vtkMath::Normalize(ax); op->RotateWXYZ(mag, ax[0], ax[1], ax[2]); } break; case ROT_Y: mag = get_rotation_magnitude(gy); { double ax[3] = {gy[0], gy[1], gy[2]}; vtkMath::Normalize(ax); op->RotateWXYZ(mag, ax[0], ax[1], ax[2]); } break; case ROT_Z: mag = get_rotation_magnitude(gz); { double ax[3] = {gz[0], gz[1], gz[2]}; vtkMath::Normalize(ax); op->RotateWXYZ(mag, ax[0], ax[1], ax[2]); } break; case SCALE_X: mag = get_motion_magnitude(gx, gpos); op->Scale(std::max(0.1, 1.0 + mag), 1.0, 1.0); // Note: Scale might need orient_inv/orient if we want to scale along local axes nicely // but the current logic for scale already handles this if we concatenated basis. // For now we keep it simple since user only reported rotation issue. break; case SCALE_Y: mag = get_motion_magnitude(gy, gpos); op->Scale(1.0, std::max(0.1, 1.0 + mag), 1.0); break; case SCALE_Z: mag = get_motion_magnitude(gz, gpos); op->Scale(1.0, 1.0, std::max(0.1, 1.0 + mag)); break; case ROT_CAM: { 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); // For camera ring, use screen-space angular delta double c_disp[3]; this->ComputeWorldToDisplay(gpos[0], gpos[1], gpos[2], c_disp); double v1[2] = {this->StartEventPosition[0] - c_disp[0], this->StartEventPosition[1] - c_disp[1]}; double v2[2] = {static_cast(X) - c_disp[0], static_cast(Y) - c_disp[1]}; if (vtkMath::Norm2D(v1) > 1.0 && vtkMath::Norm2D(v2) > 1.0) { double d_ang = vtkMath::DegreesFromRadians(atan2(v2[1], v2[0]) - atan2(v1[1], v1[0])); while (d_ang > 180) d_ang -= 360; while (d_ang < -180) d_ang += 360; mag = d_ang; } op->RotateWXYZ(mag, dir[0], dir[1], dir[2]); break; } } op->Translate(gpos[0], gpos[1], gpos[2]); // Apply op on top of the initial object state vtkNew final_t; final_t->PostMultiply(); final_t->SetMatrix(this->m_InitialTransform->GetMatrix()); final_t->Concatenate(op); vtkMatrix4x4* targetMat = this->Prop3D->GetUserMatrix(); if (targetMat) { targetMat->DeepCopy(final_t->GetMatrix()); // std::cout << "Updated UserMatrix for interaction " << this->Interaction << std::endl; } this->Prop3D->Modified(); this->UpdateGizmoPosition(); // HIGH VISIBILITY LOG std::printf("--- WIDGET Interaction: %d, Mag: %f, Pos: %f %f %f\n", Interaction, mag, gpos[0], gpos[1], gpos[2]); 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(); } void vtkHandlerWidget::PlaceWidget() { this->UpdateGizmoPosition(); } void vtkHandlerWidget::SetTransform(::vtkTransform *t) { if (!t || !this->Prop3D) return; this->Prop3D->SetUserMatrix(t->GetMatrix()); this->UpdateGizmoPosition(); } void vtkHandlerWidget::GetTransform(::vtkTransform *t) { if (!t || !this->Prop3D) return; t->SetMatrix(this->Prop3D->GetUserMatrix()); } 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 if (dir[1] > 0) actor->RotateZ(90); else if (dir[2] > 0) actor->RotateY(-90); return actor; }; 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(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[] = {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); m_AxesY = create_arrow(y, green); m_AxesZ = create_arrow(z, blue); m_RotX = create_ring(0, red); 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.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; }; double px[] = {1.2, 0, 0}, py[] = {0, 1.2, 0}, pz[] = {0, 0, 1.2}; 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; // Calculate scaling factor: min(object_bbox, 1/5 of viewport) double bboxSize = 1.0; double bounds[6]; this->Prop3D->GetBounds(bounds); if (vtkMath::AreBoundsInitialized(bounds)) { bboxSize = std::max({bounds[1] - bounds[0], bounds[3] - bounds[2], bounds[5] - bounds[4]}); } if (bboxSize < 1e-6) bboxSize = 1.0; double screenLimit = bboxSize * 2.0; // Default if no renderer if (this->CurrentRenderer) { int *sz = this->CurrentRenderer->GetSize(); if (sz[1] > 0) { double pixelSize = std::min(sz[0], sz[1]) / 5.0; vtkCamera *cam = this->CurrentRenderer->GetActiveCamera(); if (cam->GetParallelProjection()) { screenLimit = (pixelSize / (double)sz[1]) * 2.0 * cam->GetParallelScale(); } else { double dist = cam->GetDistance(); double angleRad = vtkMath::Pi() * cam->GetViewAngle() / 180.0; double viewHeightAtDist = 2.0 * dist * tan(angleRad / 2.0); screenLimit = (pixelSize / (double)sz[1]) * viewHeightAtDist; } } } double scaleFactor = std::min(bboxSize, screenLimit); vtkNew mat_gizmo; mat_gizmo->Identity(); double center[3]; 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) { ::vtkMatrix4x4 *mat = this->Prop3D->GetMatrix(); mat_gizmo->Identity(); mat_gizmo->SetElement(0, 3, mat->GetElement(0, 3)); mat_gizmo->SetElement(1, 3, mat->GetElement(1, 3)); mat_gizmo->SetElement(2, 3, mat->GetElement(2, 3)); } 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]); } // Apply scaleFactor to the gizmo matrix (only top-left 3x3) for (int j = 0; j < 3; ++j) { for (int i = 0; i < 3; ++i) { mat_gizmo->SetElement(i, j, mat_gizmo->GetElement(i, j) * scaleFactor); } } 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(); tcam->Scale(scaleFactor, scaleFactor, scaleFactor); // 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 } // namespace uLib