diff --git a/Core/Code/Controllers/mitkSlicesCoordinator.cpp b/Core/Code/Controllers/mitkSlicesCoordinator.cpp index 056075ee53..e7aceb3b2b 100644 --- a/Core/Code/Controllers/mitkSlicesCoordinator.cpp +++ b/Core/Code/Controllers/mitkSlicesCoordinator.cpp @@ -1,102 +1,102 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include #include namespace mitk { SlicesCoordinator::SlicesCoordinator(const char* machine) : StateMachine(machine), m_LinkPlanes( true ), m_MouseCursorSet( false ) { } SlicesCoordinator::~SlicesCoordinator() { } void SlicesCoordinator::AddSliceController(SliceNavigationController* snc) { if (!snc) return; m_SliceNavigationControllers.push_back(snc); OnSliceControllerAdded(snc); // notify } void SlicesCoordinator::RemoveSliceController(SliceNavigationController* snc) { if (!snc) return; // see, whether snc is in our list SNCVector::iterator iter; for (iter = m_SliceNavigationControllers.begin(); iter != m_SliceNavigationControllers.end(); ++iter) if (*iter == snc) break; // if found, remove from list if ( iter != m_SliceNavigationControllers.end() ) { m_SliceNavigationControllers.erase( iter ); OnSliceControllerRemoved(snc); } } void SlicesCoordinator::ResetMouseCursor() { if ( m_MouseCursorSet ) { ApplicationCursor::GetInstance()->PopCursor(); m_MouseCursorSet = false; } } void SlicesCoordinator::SetMouseCursor( const char *xpm[], int hotspotX, int hotspotY ) { // Remove previously set mouse cursor if ( m_MouseCursorSet ) { ApplicationCursor::GetInstance()->PopCursor(); } ApplicationCursor::GetInstance()->PushCursor( xpm, hotspotX, hotspotY ); m_MouseCursorSet = true; } void SlicesCoordinator::OnSliceControllerAdded(SliceNavigationController*) { // implement in subclasses } void SlicesCoordinator::OnSliceControllerRemoved(SliceNavigationController*) { // implement in subclasses } -bool SlicesCoordinator::ExecuteAction(Action*, StateEvent const*) +bool SlicesCoordinator::ExecuteAction(Action* a, StateEvent const* e) { - // implement in subclasses - return false; + // implement in subclasses, BUT call default implementation to make these CONNECT_ACTION statements work out + return Superclass::ExecuteAction(a,e); } } // namespace diff --git a/Core/Code/Controllers/mitkSlicesRotator.cpp b/Core/Code/Controllers/mitkSlicesRotator.cpp index c054a30c3c..d0ce45f9d3 100644 --- a/Core/Code/Controllers/mitkSlicesRotator.cpp +++ b/Core/Code/Controllers/mitkSlicesRotator.cpp @@ -1,580 +1,513 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "rotate_cursor.xpm" namespace mitk { SlicesRotator::Pointer SlicesRotator::New() { return SlicesRotator::New("slices-rotator"); } SlicesRotator::SlicesRotator(const char* machine) : SlicesCoordinator(machine) { - + // make sure that AcSWITCHON and AcSWITCHOFF are defined int constants somewhere (e.g. mitkInteractionConst.h) + CONNECT_ACTION( AcMOVE, DoSelectSlice ); + CONNECT_ACTION( AcCHECKPOINT, DoDecideBetweenRotationAndSliceSelection ); + CONNECT_ACTION( AcROTATESTART, DoStartRotation ); + CONNECT_ACTION( AcROTATE, DoRotationStep ); + CONNECT_ACTION( AcROTATEEND, DoEndRotation ); } SlicesRotator::~SlicesRotator() { } -// check if the slices of this SliceNavigationController can be rotated (???) Possible void SlicesRotator::OnSliceControllerAdded(SliceNavigationController* snc) { if (!snc) return; snc->ConnectGeometrySendEvent(this); // connects creation of new world geometry to Self::SetGeometry } void SlicesRotator::OnSliceControllerRemoved(SliceNavigationController* snc) { if (!snc) return; - // nothing to do + // nothing to do, base class does the bookkeeping } /// Is called whenever a SliceNavigationController invokes an event. Will update the list /// of SliceNavigationControllers that can handle rotation void SlicesRotator::SetGeometry(const itk::EventObject& /*EventObject*/) { // there is no way to determine the sender? // ==> update whole list of SNCs - UpdateRelevantSNCs(); + UpdateRotatableSNCs(); } void SlicesRotator::RotateToPoint( SliceNavigationController *rotationPlaneSNC, SliceNavigationController *rotatedPlaneSNC, const Point3D &point, bool linked ) { SliceNavigationController *thirdSNC = NULL; SNCVector::iterator iter; - for ( iter = m_RelevantSNCs.begin(); iter != m_RelevantSNCs.end(); ++iter ) + for ( iter = m_RotatableSNCs.begin(); iter != m_RotatableSNCs.end(); ++iter ) { if ( ((*iter) != rotationPlaneSNC) && ((*iter) != rotatedPlaneSNC) ) { thirdSNC = *iter; break; } } if ( thirdSNC == NULL ) { return; } const PlaneGeometry *rotationPlane = rotationPlaneSNC->GetCurrentPlaneGeometry(); const PlaneGeometry *rotatedPlane = rotatedPlaneSNC->GetCurrentPlaneGeometry(); const PlaneGeometry *thirdPlane = thirdSNC->GetCurrentPlaneGeometry(); if ( (rotationPlane == NULL) || (rotatedPlane == NULL) || (thirdPlane == NULL) ) { return; } if ( rotatedPlane->DistanceFromPlane( point ) < 0.001 ) { // Skip irrelevant rotations return; } Point3D projectedPoint; Line3D intersection; Point3D rotationCenter; if ( !rotationPlane->Project( point, projectedPoint ) || !rotationPlane->IntersectionLine( rotatedPlane, intersection ) || !thirdPlane->IntersectionPoint( intersection, rotationCenter ) ) { return; } // All pre-requirements are met; execute the rotation Point3D referencePoint = intersection.Project( projectedPoint ); Vector3D toProjected = referencePoint - rotationCenter; Vector3D toCursor = projectedPoint - rotationCenter; // cross product: | A x B | = |A| * |B| * sin(angle) Vector3D axisOfRotation; vnl_vector_fixed< ScalarType, 3 > vnlDirection = vnl_cross_3d( toCursor.GetVnlVector(), toProjected.GetVnlVector() ); axisOfRotation.SetVnlVector( vnlDirection ); // scalar product: A * B = |A| * |B| * cos(angle) // tan = sin / cos ScalarType angle = - atan2( (double)(axisOfRotation.GetNorm()), (double)(toCursor * toProjected) ); angle *= 180.0 / vnl_math::pi; // create RotationOperation and apply to all SNCs that should be rotated RotationOperation op(OpROTATE, rotationCenter, axisOfRotation, angle); if ( !linked ) { BaseRenderer *renderer = rotatedPlaneSNC->GetRenderer(); if ( renderer == NULL ) { return; } DisplayGeometry *displayGeometry = renderer->GetDisplayGeometry(); Point2D point2DWorld, point2DDisplayPre, point2DDisplayPost; displayGeometry->Map( rotationCenter, point2DWorld ); displayGeometry->WorldToDisplay( point2DWorld, point2DDisplayPre ); const Geometry3D *geometry3D = rotatedPlaneSNC->GetCreatedWorldGeometry(); const TimeSlicedGeometry *timeSlicedGeometry = dynamic_cast( geometry3D ); if ( !timeSlicedGeometry ) { return; } const_cast< TimeSlicedGeometry * >( timeSlicedGeometry )->ExecuteOperation( &op ); displayGeometry->Map( rotationCenter, point2DWorld ); displayGeometry->WorldToDisplay( point2DWorld, point2DDisplayPost ); Vector2D vector2DDisplayDiff = point2DDisplayPost - point2DDisplayPre; //Vector2D origin = displayGeometry->GetOriginInMM(); displayGeometry->MoveBy( vector2DDisplayDiff ); rotatedPlaneSNC->SendCreatedWorldGeometryUpdate(); } else { SNCVector::iterator iter; - for ( iter = m_RelevantSNCs.begin(); iter != m_RelevantSNCs.end(); ++iter ) + for ( iter = m_RotatableSNCs.begin(); iter != m_RotatableSNCs.end(); ++iter ) { BaseRenderer *renderer = (*iter)->GetRenderer(); if ( renderer == NULL ) { continue; } DisplayGeometry *displayGeometry = renderer->GetDisplayGeometry(); Point2D point2DWorld, point2DDisplayPre, point2DDisplayPost; displayGeometry->Map( rotationCenter, point2DWorld ); displayGeometry->WorldToDisplay( point2DWorld, point2DDisplayPre ); const Geometry3D* geometry3D = (*iter)->GetCreatedWorldGeometry(); const TimeSlicedGeometry *timeSlicedGeometry = dynamic_cast( geometry3D ); if ( !timeSlicedGeometry ) { continue; } const_cast< TimeSlicedGeometry * >( timeSlicedGeometry )->ExecuteOperation( &op ); displayGeometry->Map( rotationCenter, point2DWorld ); displayGeometry->WorldToDisplay( point2DWorld, point2DDisplayPost ); Vector2D vector2DDisplayDiff = point2DDisplayPost - point2DDisplayPre; //Vector2D origin = displayGeometry->GetOriginInMM(); displayGeometry->MoveBy( vector2DDisplayDiff ); (*iter)->SendCreatedWorldGeometryUpdate(); } } -} +} // end RotateToPoint /// Updates the list of SliceNavigationControllers that can handle rotation -void SlicesRotator::UpdateRelevantSNCs() +void SlicesRotator::UpdateRotatableSNCs() { - m_RelevantSNCs.clear(); + m_RotatableSNCs.clear(); for (SNCVector::iterator iter = m_SliceNavigationControllers.begin(); iter != m_SliceNavigationControllers.end(); ++iter) { const Geometry3D* geometry3D = (*iter)->GetCreatedWorldGeometry(); const TimeSlicedGeometry* timeSlicedGeometry = dynamic_cast( geometry3D ); if (!timeSlicedGeometry) continue; const SlicedGeometry3D* slicedGeometry = dynamic_cast( timeSlicedGeometry->GetGeometry3D(0) ); if (!slicedGeometry) continue; - Geometry2D* firstSlice(NULL); - //Geometry2D* secondSlice(NULL); - if (slicedGeometry->IsValidSlice(0)) firstSlice = slicedGeometry->GetGeometry2D(0); - //if (slicedGeometry->IsValidSlice(1)) secondSlice = slicedGeometry->GetGeometry2D(1); - - // if the direction vector of these two slices is the same, then accept this slice stack as rotatable - Vector3D right1 = firstSlice->GetAxisVector(0); - Vector3D up1 = firstSlice->GetAxisVector(1); - vnl_vector_fixed< ScalarType, 3 > vnlDirection1 = vnl_cross_3d(right1.GetVnlVector(), up1.GetVnlVector()); - Vector3D direction1; - direction1.SetVnlVector(vnlDirection1); - - Vector3D right2 = firstSlice->GetAxisVector(0); - Vector3D up2 = firstSlice->GetAxisVector(1); - vnl_vector_fixed< ScalarType, 3 > vnlDirection2 = vnl_cross_3d(right2.GetVnlVector(), up2.GetVnlVector()); - Vector3D direction2; - direction2.SetVnlVector(vnlDirection2); - - bool equal = true; - const ScalarType eps = 0.0001; - for (int i = 0; i < 3; ++i) - if ( fabs(direction1[i] - direction2[i]) > eps ) - equal = false; - - if (equal) // equal direction vectors + if (slicedGeometry->IsValidSlice(0)) { - m_RelevantSNCs.push_back( *iter ); + // there were some lines of additional checks here in previous versions, + // all of which would always evaluate to true, so the check was irrelevant. + // Since the original intent was not documented, I removed all checks, + // i.e. m_RotatableSNCs ends up being a list of all the registered + // SliceNavigationControllers which have a SlicedGeometry3D with at least one slice, + // which covers most standard cases. + m_RotatableSNCs.push_back( *iter ); } } } -bool SlicesRotator::ExecuteAction(Action* action, StateEvent const* stateEvent) +bool SlicesRotator::DoSelectSlice(Action* a, const StateEvent* e) { - const ScalarType ThreshHoldDistancePixels = 12.0; - - bool ok = false; - - switch ( action->GetActionId() ) + // just reach through + for (SNCVector::iterator iter = m_RotatableSNCs.begin(); iter != m_RotatableSNCs.end(); ++iter) { - case AcMOVE: + if ( !(*iter)->GetSliceLocked() ) { - // just reach through - for (SNCVector::iterator iter = m_RelevantSNCs.begin(); iter != m_RelevantSNCs.end(); ++iter) - { - if ( !(*iter)->GetSliceLocked() ) - { - (*iter)->ExecuteAction(action, stateEvent); - } - } - - ok = true; - break; + (*iter)->ExecuteAction(a,e); } - case AcROTATE: - { - const DisplayPositionEvent* posEvent = dynamic_cast(stateEvent->GetEvent()); - if (!posEvent) break; - - Point3D cursor = posEvent->GetWorldPosition(); - - Vector3D toProjected = m_LastCursorPosition - m_CenterOfRotation; - Vector3D toCursor = cursor - m_CenterOfRotation; - - // cross product: | A x B | = |A| * |B| * sin(angle) - Vector3D axisOfRotation; - vnl_vector_fixed< ScalarType, 3 > vnlDirection = vnl_cross_3d( toCursor.GetVnlVector(), toProjected.GetVnlVector() ); - axisOfRotation.SetVnlVector(vnlDirection); - - // scalar product: A * B = |A| * |B| * cos(angle) - // tan = sin / cos - ScalarType angle = - atan2( (double)(axisOfRotation.GetNorm()), (double)(toCursor * toProjected) ); - angle *= 180.0 / vnl_math::pi; - m_LastCursorPosition = cursor; + } - // create RotationOperation and apply to all SNCs that should be rotated - RotationOperation op(OpROTATE, m_CenterOfRotation, axisOfRotation, angle); + return true; +} - // TEST - int i = 0; +bool SlicesRotator::DoDecideBetweenRotationAndSliceSelection(Action*, const StateEvent* e) +{ + // Decide between moving and rotation slices. + // For basic decision logic see class documentation. + + /* + Detail logic: + + 1. Find the SliceNavigationController that has sent the event: this one defines our rendering plane and will NOT be rotated. Must not even be counted or checked.. + 2. Inspect every other SliceNavigationController + - calculate the line intersection of this SliceNavigationController's plane with our rendering plane + - if there is no interesection, ignore and continue + - IF there is an intersection + - check the mouse cursor's distance from that line. + 0. if the line is NOT near the cursor, remember the plane as "one of the other planes" (which can be rotated in "locked" mode) + 1. on first line near the cursor, just remember this intersection line as THE other plane that we want to rotate + 2. on every consecutive line near the cursor, check if the line is geometrically identical to the line that we want to rotate + - if yes, we just push this line to the "other" lines and rotate it along + - if no, then we have a situation where the mouse is near two other lines (e.g. crossing point) and don't want to rotate + */ + const DisplayPositionEvent* posEvent = dynamic_cast(e->GetEvent()); + if (!posEvent) return false; + + BaseRenderer* clickedRenderer = e->GetEvent()->GetSender(); + const PlaneGeometry* ourViewportGeometry = dynamic_cast( clickedRenderer->GetCurrentWorldGeometry2D() ); + if (!ourViewportGeometry) return false; - for (SNCVector::iterator iter = m_SNCsToBeRotated.begin(); iter != m_SNCsToBeRotated.end(); ++iter) - { - BaseRenderer *renderer = (*iter)->GetRenderer(); - if ( renderer == NULL ) - { - continue; - } + DisplayGeometry* clickedDisplayGeometry = clickedRenderer->GetDisplayGeometry(); + if (!clickedDisplayGeometry) return false; + + MITK_DEBUG << "============================================="; + MITK_DEBUG << "Renderer under cursor is " << clickedRenderer->GetName(); + + Point3D cursorPosition = posEvent->GetWorldPosition(); + const PlaneGeometry* geometryToBeRotated = NULL; // this one is under the mouse cursor + const PlaneGeometry* anyOtherGeometry = NULL; // this is also visible (for calculation of intersection ONLY) + Line3D intersectionLineWithGeometryToBeRotated; - DisplayGeometry *displayGeometry = renderer->GetDisplayGeometry(); + bool hitMultipleLines(false); + m_SNCsToBeRotated.clear(); - // MITK_INFO << i << ":" << std::endl; + const double threshholdDistancePixels = 12.0; + + for (SNCVector::iterator iter = m_RotatableSNCs.begin(); iter != m_RotatableSNCs.end(); ++iter) + { + const PlaneGeometry* otherRenderersRenderPlane = (*iter)->GetCurrentPlaneGeometry(); + if (otherRenderersRenderPlane == NULL) continue; // ignore, we don't see a plane + MITK_DEBUG << " Checking plane of renderer " << (*iter)->GetRenderer()->GetName(); + + // check if there is an intersection + Line3D intersectionLine; // between rendered/clicked geometry and the one being analyzed + if (!ourViewportGeometry->IntersectionLine( otherRenderersRenderPlane, intersectionLine )) + { + continue; // we ignore this plane, it's parallel to our plane + } - Point2D point2DWorld, point2DDisplayPre, point2DDisplayPost; - displayGeometry->Map( m_CenterOfRotation, point2DWorld ); - displayGeometry->WorldToDisplay( point2DWorld, point2DDisplayPre ); + // check distance from intersection line + double distanceFromIntersectionLine = intersectionLine.Distance( cursorPosition ); + ScalarType distancePixels = distanceFromIntersectionLine / clickedDisplayGeometry->GetScaleFactorMMPerDisplayUnit(); + MITK_DEBUG << " Distance of plane from cursor " << distanceFromIntersectionLine << " mm, which is around " << distancePixels << " px" ; - // MITK_INFO << " WorldPre: " << point2DWorld << " / DisplayPre: " << point2DDisplayPre << std::endl; + // far away line, only remember for linked rotation if necessary + if (distanceFromIntersectionLine > threshholdDistancePixels) + { + MITK_DEBUG << " Plane is too far away --> remember as otherRenderersRenderPlane"; + anyOtherGeometry = otherRenderersRenderPlane; // we just take the last one, so overwrite each iteration (we just need some crossing point) + // TODO what about multiple crossings? NOW we have undefined behavior / random crossing point is used - const Geometry3D* geometry3D = (*iter)->GetCreatedWorldGeometry(); - const TimeSlicedGeometry* timeSlicedGeometry = dynamic_cast(geometry3D); - if (!timeSlicedGeometry) continue; - - const_cast(timeSlicedGeometry)->ExecuteOperation(&op); + if (m_LinkPlanes) + { + m_SNCsToBeRotated.push_back(*iter); + } + } + else // close to cursor + { + MITK_DEBUG << " Plane is close enough to cursor..."; + if ( geometryToBeRotated == NULL ) // first one close to the cursor + { + MITK_DEBUG << " It is the first close enough geometry, remember as geometryToBeRotated"; + geometryToBeRotated = otherRenderersRenderPlane; + intersectionLineWithGeometryToBeRotated = intersectionLine; + m_SNCsToBeRotated.push_back(*iter); + } + else + { + MITK_DEBUG << " Second or later close enough geometry"; + // compare to the line defined by geometryToBeRotated: if identical, just rotate this otherRenderersRenderPlane together with the primary one + // if different, DON'T rotate - //vtkLinearTransform *inverseTransformVtk = - // displayGeometry->GetVtkTransform()->GetLinearInverse(); + if ( intersectionLine.IsParallel( intersectionLineWithGeometryToBeRotated ) + && intersectionLine.Distance( intersectionLineWithGeometryToBeRotated.GetPoint1() ) < mitk::eps ) + { + MITK_DEBUG << " This line is the same as intersectionLineWithGeometryToBeRotated which we already know"; + m_SNCsToBeRotated.push_back(*iter); + } + else + { + MITK_DEBUG << " This line is NOT the same as intersectionLineWithGeometryToBeRotated which we already know"; + hitMultipleLines = true; + } + } + } - //ScalarType pvtkCenterOfRotation[3]; - //pvtkCenterOfRotation[0] = m_CenterOfRotation[0]; - //pvtkCenterOfRotation[1] = m_CenterOfRotation[1]; - //pvtkCenterOfRotation[2] = m_CenterOfRotation[2]; + } - //ScalarType scaleFactorMMPerUnitX = - // displayGeometry->GetExtentInMM(0) / displayGeometry->GetExtent(0); - //ScalarType scaleFactorMMPerUnitY = - // displayGeometry->GetExtentInMM(1) / displayGeometry->GetExtent(1); + bool moveSlices(true); - //ScalarType scaleFactorMMPerDisplayUnit = displayGeometry->GetScaleFactorMMPerDisplayUnit(); - //Vector2D &originInMM = displayGeometry->GetOriginInMM(); + if ( geometryToBeRotated && anyOtherGeometry && ourViewportGeometry && !hitMultipleLines ) + { + // assure all three are valid, so calculation of center of rotation can be done + moveSlices = false; + } - ////displayGeometry->Map( m_CenterOfRotation, point2DWorld ); + MITK_DEBUG << "geometryToBeRotated: " << (void*)geometryToBeRotated; + MITK_DEBUG << "anyOtherGeometry: " << (void*)anyOtherGeometry; + MITK_DEBUG << "ourViewportGeometry: " << (void*)ourViewportGeometry; + MITK_DEBUG << "hitMultipleLines? " << hitMultipleLines; + MITK_DEBUG << "moveSlices? " << moveSlices; - //ScalarType pvtkDisplayPost[3]; - //inverseTransformVtk->TransformPoint( pvtkCenterOfRotation, pvtkDisplayPost ); - //pvtkDisplayPost[0] *= scaleFactorMMPerUnitX; - //pvtkDisplayPost[1] *= scaleFactorMMPerUnitY; + std::auto_ptr decidedEvent; - ////displayGeometry->WorldToDisplay( point2DWorld, point2DDisplayPost ); - //pvtkDisplayPost[0] -= originInMM[0]; - //pvtkDisplayPost[1] -= originInMM[1]; + // question in state machine is: "rotate?" + if (moveSlices) // i.e. NOT rotate + { + // move all planes to posEvent->GetWorldPosition() + decidedEvent.reset( new StateEvent(EIDNO, e->GetEvent()) ); + MITK_DEBUG << "Rotation not possible, not enough information (other planes crossing rendering plane) "; + } + else + { // we DO have enough information for rotation + m_LastCursorPosition = intersectionLineWithGeometryToBeRotated.Project(cursorPosition); // remember where the last cursor position ON THE LINE has been observed - //pvtkDisplayPost[0] /= scaleFactorMMPerDisplayUnit; - //pvtkDisplayPost[1] /= scaleFactorMMPerDisplayUnit; + if (anyOtherGeometry->IntersectionPoint(intersectionLineWithGeometryToBeRotated, m_CenterOfRotation)) // find center of rotation by intersection with any of the OTHER lines + { + decidedEvent.reset( new StateEvent(EIDYES, e->GetEvent()) ); + MITK_DEBUG << "Rotation possible"; + } + else + { + MITK_DEBUG << "Rotation not possible, cannot determine the center of rotation!?"; + decidedEvent.reset( new StateEvent(EIDNO, e->GetEvent()) ); + } + } - //point2DDisplayPost[0] = pvtkDisplayPost[0]; - //point2DDisplayPost[1] = pvtkDisplayPost[1]; + this->HandleEvent( decidedEvent.get() ); + return true; +} - displayGeometry->Map( m_CenterOfRotation, point2DWorld ); - displayGeometry->WorldToDisplay( point2DWorld, point2DDisplayPost ); - Vector2D vector2DDisplayDiff = point2DDisplayPost - point2DDisplayPre; +bool SlicesRotator::DoStartRotation(Action*, const StateEvent*) +{ + this->SetMouseCursor( rotate_cursor_xpm, 0, 0 ); + this->InvokeEvent( SliceRotationEvent() ); // notify listeners + return true; +} - //Vector2D origin = displayGeometry->GetOriginInMM(); - // MITK_INFO << " WorldPost: " << point2DWorld << " / DisplayPost: " << point2DDisplayPost << std::endl; - // MITK_INFO << " Diff - " << vector2DDisplayDiff << std::endl; - // MITK_INFO << " Origin - " << origin << std::endl; - ++i; +bool SlicesRotator::DoEndRotation(Action*, const StateEvent*) +{ + this->ResetMouseCursor(); + this->InvokeEvent( SliceRotationEvent() ); // notify listeners + return true; +} - displayGeometry->MoveBy( vector2DDisplayDiff ); - (*iter)->SendCreatedWorldGeometryUpdate(); - } - // MITK_INFO << "--------------------------------" << std::endl; +bool SlicesRotator::DoRotationStep(Action*, const StateEvent* e) +{ + const DisplayPositionEvent* posEvent = dynamic_cast(e->GetEvent()); + if (!posEvent) return false; - - - // TEST - //BaseRenderer* renderer = stateEvent->GetEvent()->GetSender(); // TODO this is NOT SNC-specific! Should be! - // - //DisplayGeometry* displayGeometry = renderer->GetDisplayGeometry(); - //if (!displayGeometry) break; + Point3D cursor = posEvent->GetWorldPosition(); - //Point2D point2DWorld, point2DDisplay; - //displayGeometry->Map( m_CenterOfRotation, point2DWorld ); - //displayGeometry->WorldToDisplay( point2DWorld, point2DDisplay ); + Vector3D toProjected = m_LastCursorPosition - m_CenterOfRotation; + Vector3D toCursor = cursor - m_CenterOfRotation; - //MITK_INFO << "RotationCenter: " << m_CenterOfRotation << std::endl; - //MITK_INFO << "PointWorld: " << point2DWorld << std::endl; - //MITK_INFO << "PointDisplay: " << point2DDisplay << std::endl; - //MITK_INFO << "--------------------------------------------" << std::endl; + // cross product: | A x B | = |A| * |B| * sin(angle) + Vector3D axisOfRotation; + vnl_vector_fixed< ScalarType, 3 > vnlDirection = vnl_cross_3d( toCursor.GetVnlVector(), toProjected.GetVnlVector() ); + axisOfRotation.SetVnlVector(vnlDirection); + // scalar product: A * B = |A| * |B| * cos(angle) + // tan = sin / cos + ScalarType angle = - atan2( (double)(axisOfRotation.GetNorm()), (double)(toCursor * toProjected) ); + angle *= 180.0 / vnl_math::pi; + m_LastCursorPosition = cursor; + // create RotationOperation and apply to all SNCs that should be rotated + RotationOperation rotationOperation(OpROTATE, m_CenterOfRotation, axisOfRotation, angle); - RenderingManager::GetInstance()->RequestUpdateAll(); - - this->InvokeEvent( SliceRotationEvent() ); // notify listeners - - ok = true; - break; - } - case AcCHECKPOINT: - { - // decide between moving and rotation - - // Alle SNCs (Anzahl N) nach dem Abstand von posEvent->GetWorldPosition() zur aktuellen Ebene fragen. - // Anzahl der Ebenen zaehlen, die naeher als ein gewisser Schwellwertwert sind -> nNah. - // Wenn nNah == N - // Generiere ein PointEvent und schicke das an alle SNCs -> bewege den kreuz-mittelpunkt - // Wenn nNah == 2 - // Streiche stateEvent->Sender aus der Liste der nahen Ebenen - // fuer die uebrigen generiere eine RotationOperation und fuehre die aus - // sonst - // - const DisplayPositionEvent* posEvent = dynamic_cast(stateEvent->GetEvent()); - if (!posEvent) break; - - Point3D cursor = posEvent->GetWorldPosition(); - //m_LastCursorPosition = cursor; - - unsigned int numNearPlanes = 0; - m_SNCsToBeRotated.clear(); - Geometry2D* geometryToBeRotated = NULL; // this one is grabbed - Geometry2D* otherGeometry = NULL; // this is also visible (for calculation of intersection) - Geometry2D* clickedGeometry = NULL; // the event originates from this one - //SlicedGeometry3D* clickedSlicedGeometry; - - for (SNCVector::iterator iter = m_RelevantSNCs.begin(); iter != m_RelevantSNCs.end(); ++iter) - { - unsigned int slice = (*iter)->GetSlice()->GetPos(); - unsigned int time = (*iter)->GetTime()->GetPos(); - - const Geometry3D* geometry3D = (*iter)->GetCreatedWorldGeometry(); - const TimeSlicedGeometry* timeSlicedGeometry = dynamic_cast( geometry3D ); - if (!timeSlicedGeometry) continue; + // iterate the OTHER slice navigation controllers: these are filled in DoDecideBetweenRotationAndSliceSelection + for (SNCVector::iterator iter = m_SNCsToBeRotated.begin(); iter != m_SNCsToBeRotated.end(); ++iter) + { + // - remember the center of rotation on the 2D display BEFORE rotation + // - execute rotation + // - calculate new center of rotation on 2D display + // - move display IF the center of rotation has moved slightly before and after rotation - const SlicedGeometry3D* slicedGeometry = dynamic_cast( timeSlicedGeometry->GetGeometry3D(time) ); - if (!slicedGeometry) continue; + // DM 2012-10: this must probably be due to rounding errors only, right? + // We don't have documentation on if/why this code is needed + BaseRenderer *renderer = (*iter)->GetRenderer(); + if ( !renderer ) continue; - Geometry2D* geometry2D = slicedGeometry->GetGeometry2D(slice); - if (!geometry2D) continue; // this is not necessary? + DisplayGeometry *displayGeometry = renderer->GetDisplayGeometry(); - ScalarType distanceMM = geometry2D->Distance( cursor ); + Point2D rotationCenter2DWorld, point2DDisplayPreRotation, point2DDisplayPostRotation; + displayGeometry->Map( m_CenterOfRotation, rotationCenter2DWorld ); + displayGeometry->WorldToDisplay( rotationCenter2DWorld, point2DDisplayPreRotation ); - BaseRenderer* renderer = stateEvent->GetEvent()->GetSender(); // TODO this is NOT SNC-specific! Should be! - - DisplayGeometry* displayGeometry = renderer->GetDisplayGeometry(); - if (!displayGeometry) continue; - - ScalarType distancePixels = distanceMM / displayGeometry->GetScaleFactorMMPerDisplayUnit(); - if ( distancePixels <= ThreshHoldDistancePixels ) - { - ++numNearPlanes; // count this one as a plane near to the cursor - } - - if ( *iter == renderer->GetSliceNavigationController() ) // don't rotate the one where the user clicked - { - clickedGeometry = geometry2D; - //clickedSlicedGeometry = const_cast(slicedGeometry); - } - else - { - // @TODO here waits some bug to be found - maybe fixed by the || m_LinkPlanes in next line - if ( (distancePixels <= ThreshHoldDistancePixels) - && !(*iter)->GetSliceRotationLocked() - && (m_SNCsToBeRotated.empty() || m_LinkPlanes) ) - { - // this one is behind the clicked "line" - m_SNCsToBeRotated.push_back(*iter); - geometryToBeRotated = geometry2D; - } - else - { - otherGeometry = geometry2D; - - if ( m_LinkPlanes ) - { - // All slices are rotated, i.e. the relative angles between - // slices remain fixed - m_SNCsToBeRotated.push_back(*iter); - } - } - } - } + const Geometry3D* geometry3D = (*iter)->GetCreatedWorldGeometry(); + const TimeSlicedGeometry* timeSlicedGeometry = dynamic_cast(geometry3D); + if (!timeSlicedGeometry) continue; - bool move (true); + const_cast(timeSlicedGeometry)->ExecuteOperation(&rotationOperation); - if ( geometryToBeRotated && otherGeometry && clickedGeometry - && ( numNearPlanes == 2 ) ) - { - // assure all three are valid, so calculation of center of rotation can be done - move = false; - } - - std::auto_ptr newStateEvent; + displayGeometry->Map( m_CenterOfRotation, rotationCenter2DWorld ); + displayGeometry->WorldToDisplay( rotationCenter2DWorld, point2DDisplayPostRotation ); + Vector2D vector2DDisplayDiff = point2DDisplayPostRotation - point2DDisplayPreRotation; - // question in state machine is: "rotate?" - if (move) - { - // move all planes to posEvent->GetWorldPosition() - newStateEvent.reset(new StateEvent(EIDNO, stateEvent->GetEvent())); - } - else - { - // determine center of rotation TODO requires two plane geometries... - PlaneGeometry* planeGeometry = dynamic_cast(clickedGeometry); - PlaneGeometry* planeGeometry1 = dynamic_cast(geometryToBeRotated); - PlaneGeometry* planeGeometry2 = dynamic_cast(otherGeometry); - - if (!planeGeometry || !planeGeometry1 || !planeGeometry2) break; - - Line3D intersection; - if (!planeGeometry->IntersectionLine( planeGeometry1, intersection )) break; - m_LastCursorPosition = intersection.Project(cursor); - if (!planeGeometry2->IntersectionPoint(intersection, m_CenterOfRotation)) break; - // everything's fine - newStateEvent.reset(new StateEvent(EIDYES, stateEvent->GetEvent())); + displayGeometry->MoveBy( vector2DDisplayDiff ); - } + (*iter)->SendCreatedWorldGeometryUpdate(); + } - if (!newStateEvent.get()) MITK_ERROR << "rotation would be nice but is impossible... " << std::endl; - - this->HandleEvent( newStateEvent.get() ); + RenderingManager::GetInstance()->RequestUpdateAll(); - ok = true; - break; - } - case AcROTATESTART: - { - this->SetMouseCursor( rotate_cursor_xpm, 0, 0 ); - this->InvokeEvent( SliceRotationEvent() ); // notify listeners - break; - } - case AcROTATEEND: - { - this->ResetMouseCursor(); - this->InvokeEvent( SliceRotationEvent() ); // notify listeners - break; - } - default: - { - break; - } - } + this->InvokeEvent( SliceRotationEvent() ); // notify listeners - return ok; + return true; } } // namespace diff --git a/Core/Code/Controllers/mitkSlicesRotator.h b/Core/Code/Controllers/mitkSlicesRotator.h index cf02880eba..9b737b30ac 100644 --- a/Core/Code/Controllers/mitkSlicesRotator.h +++ b/Core/Code/Controllers/mitkSlicesRotator.h @@ -1,110 +1,165 @@ /*=================================================================== The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center, Division of Medical and Biological Informatics. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE.txt or http://www.mitk.org for details. ===================================================================*/ #ifndef SLICESROTATOR_H_HEADER_INCLUDED_C1C55A2F #define SLICESROTATOR_H_HEADER_INCLUDED_C1C55A2F #include #pragma GCC visibility push(default) #include #pragma GCC visibility pop #include namespace mitk { /** - * \brief Enables rotation of visible slices (for sliced geometries). - * \ingroup NavigationControl - * - * This class takes care of several SliceNavigationControllers and handles - * slice selection / slice rotation. It is added as listener to - * GlobalInteraction by QmitkStdMultiWidget. - * - * The SlicesRotator class adds the possibility of slice rotation to the - * "normal" behaviour of SliceNavigationControllers. This additional class - * is needed, because one has to be aware of several "visible slices" - * (selected Geometry2Ds of some SliceNavigationControllers) in order to - * choose between rotation and slice selection. - * - * Rotation is achieved by modifying (rotating) the generated - * TimeSlicedGeometry of the corresponding SliceNavigationController. - * - * With SlicesRotator, the rule to choose between slice rotation and - * selection is simple: For a mouse down event, count the number of visible - * planes, which are "near" the cursor. If this number equals 2 (one for the - * window, which currently holds the cursor, one for the intersection line of - * another visible slice), then initiate rotation, else select slices near - * the cursor. If "LinkPlanes" is set to true, the rotation is applied to the - * planes of all registered SNCs, not only of the one associated with the - * directly selected plane. - * - * In contrast to the situation without the SlicesRotator, the - * SliceNavigationControllers are now not directly registered as listeners to - * GlobalInteraction. SlicesRotator is registered as a listener and decides - * whether something should be rotated or whether another slice should be - * selected. In the latter case, a PositionEvent is just forwarded to the - * SliceNavigationController. - * - * \sa SlicesSwiveller + \brief Coordinates rotation of multiple visible rendering planes (represented as lines in other render windows). + \ingroup NavigationControl + + This class takes care of several SliceNavigationControllers and handles slice selection + / slice rotation. It is added as listener to GlobalInteraction by QmitkStdMultiWidget. + + The SlicesRotator class adds the possibility of slice rotation to the "normal" behaviour + of SliceNavigationControllers (which is picking one plane from a stack of planes). + + This additional class SlicesRotator is needed, because one has to be aware of multiple + "visible slices" (selected Geometry2Ds of some SliceNavigationControllers) in order to + choose between rotation and slice selection. Such functionality could not be implemented + by a single SliceNavigationController. + + Rotation is achieved by modifying (rotating) the generated TimeSlicedGeometry of the + corresponding SliceNavigationControllers. + + \section mitkSlicesRotator_StandardCase The standard case: three orthogonal views (MPR) + + With SlicesRotator, the rule to choose between slice rotation and selection is simple: + For a mouse down event, count the number of visible planes, which are "near" the cursor. + If this number is 2 (one for the window, which currently holds the cursor, one for the + intersection line of another visible slice), then initiate rotation, else select slices + near the cursor. If the "LinkPlanes" flag is set, the rotation is applied to the planes + of all registered SNCs, not only of the one associated with the directly selected plane. + + In contrast to the situation without the SlicesRotator, the SliceNavigationControllers + are now NOT directly registered as listeners to GlobalInteraction. SlicesRotator is + registered as a listener and decides whether something should be rotated or whether + another slice should be selected. In the latter case, a PositionEvent is just forwarded + to the SliceNavigationController. + + \section mitkSlicesRotator_GeneralizedCase The generalized case: any number of views + + Above section as well as the original implementation of this class assumes that we have + exactly three 2D vies in our scene. This used to be the standard setup of the MITK + associated application for a long time. With custom applications based on MITK it is + easy to create different situations. One usual use case would be to have one extra + render window display the contents of any of the other ones and behave exactly like it + (could e.g. be used on a second screen). + + In this situation the above assumption "we rotate when there are exactly 2 slices close + to the cursor" will not hold: since we always have two render windows displaying the + exact same slice, the number of 2 is the minimum we get. Whenever the user clicks in one + of those windows and the cursor is close to one of the orthogonal planes, we will get a + count of 3 or more planes that are "close to the cursor". + + For the class to behave correctly, we actually need to distinguish three separate cases: + 1. the cursor is not close to any orthogonal planes. This should result in slice selection. + 2. the cursor is close to just one orthogonal plane OR multiple which are not distinguishable visually. This should result in rotation. + 3. the cursor is close to multiple orthogonal planes which are rendered as distinguishable lines on the render window. This is the case when we hit the crosshair-center of the view. In this case, we need to also just select slices. + + \section mitkSlicesRotator_Solution Deciding between slice selection and rotation + + The "counting nearby lines in the renderwindow" can also work for the general case + described above. Only one details needs to be accounted for: we must not count a line + when it is identical to another line. I.e. we just count how many visible lines on the + screen are very close to the cursor. When this number is 1, we rotate, otherwise we let + the SliceNavigationControllers do their slice selection job. + + \sa SlicesSwiveller */ class MITK_CORE_EXPORT SlicesRotator : public SlicesCoordinator { -public: + public: - mitkClassMacro(SlicesRotator, SlicesCoordinator); - - static Pointer New(); + mitkClassMacro(SlicesRotator, SlicesCoordinator); + + static Pointer New(); + + /** + \brief New Macro with one parameter for creating this object with static New(..) method. + + Needs to be the "slices-rotator" pattern of StateMachine.xml to work as expected. + **/ + mitkNewMacro1Param(Self, const char*); + + /** + \brief Callback for modifications in observed SliceNavigationControllers -- forwards to UpdateRotatableSNCs(). - /** - * @brief New Macro with one parameter for creating this object with static New(..) method - **/ - mitkNewMacro1Param(Self, const char*); + This method is called when an observed SliceNavigationController changes its + world geometry. The connection is established by calling the other SliceNavigationController's + method ConnectGeometrySendEvent (or similar). + */ + virtual void SetGeometry(const itk::EventObject& EventObject); - virtual void SetGeometry(const itk::EventObject& EventObject); + /** + \brief NOT USED by anything open-source. - virtual void RotateToPoint( SliceNavigationController *rotationPlaneSNC, - SliceNavigationController *rotatedPlaneSNC, - const Point3D &point, bool linked = false ); + \TODO check if this is actually still needed, with SliceNavigationController::ReorientSlices() now being implemented this could be obsolete!? + */ + virtual void RotateToPoint( SliceNavigationController *rotationPlaneSNC, + SliceNavigationController *rotatedPlaneSNC, + const Point3D &point, + bool linked = false ); -protected: + protected: - SlicesRotator(const char* machine); - // clear list of controllers - virtual ~SlicesRotator(); + SlicesRotator(const char* machine); + virtual ~SlicesRotator(); - // check if the slices of this SliceNavigationController can be rotated (???) Possible - virtual void OnSliceControllerAdded(SliceNavigationController* snc); + /** + \brief Called from SlicesCoordinator after a new controller is added (to internal list m_SliceNavigationControllers). + */ + virtual void OnSliceControllerAdded(SliceNavigationController* snc); - virtual void OnSliceControllerRemoved(SliceNavigationController* snc); + /* + \brief Called from SlicesCoordinator after a new controller is being removed (to internal list m_SliceNavigationControllers). + */ + virtual void OnSliceControllerRemoved(SliceNavigationController* snc); - virtual void UpdateRelevantSNCs(); + /** + \brief Check all observed SliceNavigationControllers: remember those that are rotatable in m_RotatableSNCs. + */ + virtual void UpdateRotatableSNCs(); - virtual bool ExecuteAction(Action * action, StateEvent const* stateEvent); + // following methods called from superclass ExecuteAction + bool DoSelectSlice(Action*, const StateEvent*); + bool DoDecideBetweenRotationAndSliceSelection(Action*, const StateEvent*); + bool DoStartRotation(Action*, const StateEvent*); + bool DoEndRotation(Action*, const StateEvent*); + bool DoRotationStep(Action*, const StateEvent*); - SNCVector m_RelevantSNCs; /// all SNCs that currently have CreatedWorldGeometries, that can be rotated. - SNCVector m_SNCsToBeRotated; /// all SNCs that will be rotated + SNCVector m_RotatableSNCs; /// all SNCs that currently have CreatedWorldGeometries, that can be rotated. + SNCVector m_SNCsToBeRotated; /// all SNCs that will be rotated (exceptions are the ones parallel to the one being clicked) - Point3D m_LastCursorPosition; - Point3D m_CenterOfRotation; + Point3D m_LastCursorPosition; /// used for calculation of the rotation angle + Point3D m_CenterOfRotation; /// used for calculation of the rotation angle }; } // namespace #endif