/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "StdAfx.h" #include "RotateTool.h" #include "Objects/DisplayContext.h" #include "IDisplayViewport.h" #include "NullEditTool.h" #include "Viewport.h" #include "functor.h" #include "Settings.h" #include "Grid.h" #include "ViewManager.h" #include "ISystem.h" #include #include // This constant is used with GetScreenScaleFactor and was found experimentally. static const float kViewDistanceScaleFactor = 0.06f; const GUID& CRotateTool::GetClassID() { // {A50E5B95-05B9-41A3-8D8E-BDA3E930A396} static const GUID guid = { 0xA50E5B95, 0x05B9, 0x41A3, { 0x8D, 0x8E, 0xBD, 0xA3, 0xE9, 0x30, 0xA3, 0x96 } }; return guid; } //! This method returns the human readable name of the class. //! This method returns Category of this class, Category is specifying where this tool class fits best in create panel. void CRotateTool::RegisterTool(CRegistrationContext& rc) { rc.pClassFactory->RegisterClass(new CQtViewClass("EditTool.Rotate", "Select", ESYSTEM_CLASS_EDITTOOL)); } CRotateTool::CRotateTool(CBaseObject* pObject, QWidget* parent /*= nullptr*/) : CObjectMode(parent) , m_initialViewAxisAngleRadians(0.f) , m_angleToCursor(0.f) , m_highlightAxis(AxisNone) , m_draggingMouse(false) , m_lastPosition(0, 0) , m_rotationAngles(0, 0, 0) , m_object(pObject) , m_bTransformChanged(false) , m_totalRotationAngle(0.f) , m_basisAxisRadius(4.f) , m_viewAxisRadius(5.f) , m_arcRotationStepRadians(DEG2RAD(5.f)) { m_axes[AxisX] = RotationDrawHelper::Axis(Col_Red, Col_Yellow); m_axes[AxisY] = RotationDrawHelper::Axis(Col_Green, Col_Yellow); m_axes[AxisZ] = RotationDrawHelper::Axis(Col_Blue, Col_Yellow); m_axes[AxisView] = RotationDrawHelper::Axis(Col_White, Col_Yellow); if (m_object) { m_object->AddEventListener(functor(*this, &CRotateTool::OnObjectEvent)); } GetIEditor()->GetObjectManager()->SetSelectCallback(this); } bool CRotateTool::OnSelectObject(CBaseObject* object) { m_object = object; if (m_object) { m_object->AddEventListener(functor(*this, &CRotateTool::OnObjectEvent)); } return true; } bool CRotateTool::CanSelectObject(CBaseObject* object) { return true; } void CRotateTool::OnObjectEvent(CBaseObject* object, int event) { if (event == CBaseObject::ON_DELETE || event == CBaseObject::ON_UNSELECT) { if (m_object && m_object == object) { m_object->RemoveEventListener(functor(*this, &CRotateTool::OnObjectEvent)); m_object = nullptr; } } } void CRotateTool::Display(DisplayContext& dc) { if (!m_object) { return; } const bool visible = !m_object->IsHidden() && !m_object->IsFrozen() && m_object->IsSelected(); if (!visible) { GetIEditor()->SetEditTool(new NullEditTool()); return; } RotationDrawHelper::DisplayContextScope displayContextScope(dc); m_hc.camera = dc.camera; m_hc.view = dc.view; m_hc.b2DViewport = static_cast(dc.view)->GetType() != ET_ViewportCamera; dc.SetLineWidth(m_lineThickness); // Calculate the screen space position from which we cast a ray (center of viewport). int viewportWidth = 0; int viewportHeight = 0; dc.view->GetDimensions(&viewportWidth, &viewportHeight); m_hc.point2d = QPoint(viewportWidth / 2, viewportHeight / 2); // Calculate the ray from the camera position to the selection. dc.view->ViewToWorldRay(m_hc.point2d, m_hc.raySrc, m_hc.rayDir); Matrix34 objectTransform = GetTransform(GetIEditor()->GetReferenceCoordSys(), dc.view); AffineParts ap; ap.Decompose(objectTransform); Vec3 position = ap.pos; CSelectionGroup* selection = GetIEditor()->GetSelection(); if (selection->GetCount() > 1) { position = selection->GetCenter(); } float screenScale = GetScreenScale(dc.view, dc.camera); // X axis arc Vec3 cameraViewDir = (m_hc.raySrc - position).GetNormalized(); float cameraAngle = atan2f(cameraViewDir.y, cameraViewDir.x); m_axes[AxisX].Draw(dc, position, ap.rot.GetColumn0(), cameraAngle, m_arcRotationStepRadians, m_basisAxisRadius, m_highlightAxis == AxisX, m_object, screenScale); // Y axis arc cameraAngle = atan2f(-cameraViewDir.z, cameraViewDir.x); m_axes[AxisY].Draw(dc, position, ap.rot.GetColumn1(), cameraAngle, m_arcRotationStepRadians, m_basisAxisRadius, m_highlightAxis == AxisY, m_object, screenScale); // View direction axis Vec3 cameraPos = dc.camera->GetPosition(); Vec3 axis = cameraPos - position; axis.NormalizeSafe(); // Z axis arc cameraAngle = atan2f(axis.y, axis.x); m_axes[AxisZ].Draw(dc, position, objectTransform.GetColumn2().GetNormalized(), cameraAngle, m_arcRotationStepRadians, m_basisAxisRadius, m_highlightAxis == AxisZ, m_object, screenScale); // FIXME: currently, rotating multiple selections using the view axis may result in severe rotation artifacts, it's necessary to make sure // the calculated rotation angle is smooth. if (!m_hc.b2DViewport && selection->GetCount() == 1 || m_object->CheckFlags(OBJFLAG_IS_PARTICLE)) { // Draw view direction axis dc.SetColor(m_highlightAxis == AxisView ? Col_Yellow : Col_White); cameraViewDir = m_hc.camera->GetViewdir().normalized(); dc.DrawArc(position, m_viewAxisRadius * GetScreenScale(dc.view, dc.camera), 0, 360.f, RAD2DEG(m_arcRotationStepRadians), cameraViewDir); } // Draw angle decorator if (RotationControlConfiguration::Get().RotationControl_DrawDecorators) { DrawAngleDecorator(dc); } // Display total rotation angle in degrees. if (!m_hc.b2DViewport && fabs(m_totalRotationAngle) > FLT_EPSILON) { QString label; label = QString::number(RAD2DEG(m_totalRotationAngle), 'f', 2); const float textScale = 1.5f; const ColorF textBackground = ColorF(0.2f, 0.2f, 0.2f, 0.6f); if (m_object->CheckFlags(OBJFLAG_IS_PARTICLE)) { dc.DrawTextLabel(ap.pos, textScale, label.toUtf8().data()); } else { dc.DrawTextOn2DBox(ap.pos, label.toUtf8().data(), textScale, Col_White, textBackground); } } // Draw debug diagnostics if (RotationControlConfiguration::Get().RotationControl_DebugHitTesting) { DrawHitTestGeometry(dc, m_hc); } // Draw debug tracking of the view direction angle if (RotationControlConfiguration::Get().RotationControl_AngleTracking) { DrawViewDirectionAngleTracking(dc, m_hc); } } void CRotateTool::DrawAngleDecorator(DisplayContext& dc) { if (m_highlightAxis == AxisView) { //Vec3 cameraViewDir = dc.view->GetViewTM().GetColumn1().GetNormalized(); Vec3 cameraViewDir = dc.camera->GetViewMatrix().GetColumn1().GetNormalized(); //Get the viewDir from the camera instead of from the view // FIXME: The angle and sweep calculation here is incorrect. float cameraAngle = atan2f(cameraViewDir.y, -cameraViewDir.x); float angle = m_initialViewAxisAngleRadians - cameraAngle - (g_PI / 2); float angleDelta = (m_angleToCursor - g_PI2 * floor(m_initialViewAxisAngleRadians / g_PI2)) - (m_initialViewAxisAngleRadians - (cameraAngle - (g_PI / 2))); RotationDrawHelper::AngleDecorator::Draw(dc, m_object->GetWorldPos(), cameraViewDir, m_initialViewAxisAngleRadians, angleDelta, m_arcRotationStepRadians, m_viewAxisRadius, GetScreenScale(dc.view, dc.camera)); } else { if (fabs(m_totalRotationAngle) > FLT_EPSILON) { float screenScale = GetScreenScale(dc.view, dc.camera); switch (m_highlightAxis) { case AxisX: RotationDrawHelper::AngleDecorator::Draw(dc, m_object->GetWorldPos(), m_object->GetRotation().GetColumn0(), m_initialViewAxisAngleRadians, m_totalRotationAngle, m_arcRotationStepRadians, m_basisAxisRadius, screenScale); break; case AxisY: RotationDrawHelper::AngleDecorator::Draw(dc, m_object->GetWorldPos(), m_object->GetRotation().GetColumn1(), m_initialViewAxisAngleRadians, m_totalRotationAngle, m_arcRotationStepRadians, m_basisAxisRadius, screenScale); break; case AxisZ: RotationDrawHelper::AngleDecorator::Draw(dc, m_object->GetWorldPos(), m_object->GetRotation().GetColumn2(), m_initialViewAxisAngleRadians, m_totalRotationAngle, m_arcRotationStepRadians, m_basisAxisRadius, screenScale); break; default: break; } } } } bool CRotateTool::HitTest(CBaseObject* object, HitContext& hc) { if (!m_object) { return CObjectMode::HitTest(object, hc); } m_hc = hc; m_highlightAxis = AxisNone; float screenScale = GetScreenScale(hc.view, hc.camera); // Determine intersection with the axis view direction. CSelectionGroup* selection = GetIEditor()->GetSelection(); if (!m_hc.b2DViewport && selection->GetCount() == 1 || m_object->CheckFlags(OBJFLAG_IS_PARTICLE)) { if (m_axes[AxisView].HitTest(object, hc, m_viewAxisRadius, m_arcRotationStepRadians, hc.camera ? hc.camera->GetViewMatrix().GetInverted().GetColumn1() : hc.view->GetViewTM().GetColumn1(), screenScale)) { m_highlightAxis = AxisView; GetIEditor()->SetAxisConstraints(AXIS_XYZ); return true; } } // Determine any intersection with a major axis. AffineParts ap; ap.Decompose(GetTransform(GetIEditor()->GetReferenceCoordSys(), hc.view)); if (m_axes[AxisX].HitTest(object, hc, m_basisAxisRadius, m_arcRotationStepRadians, ap.rot.GetColumn0(), screenScale)) { m_highlightAxis = AxisX; GetIEditor()->SetAxisConstraints(AXIS_X); return true; } if (m_axes[AxisY].HitTest(object, hc, m_basisAxisRadius, m_arcRotationStepRadians, ap.rot.GetColumn1(), screenScale)) { m_highlightAxis = AxisY; GetIEditor()->SetAxisConstraints(AXIS_Y); return true; } if (m_axes[AxisZ].HitTest(object, hc, m_basisAxisRadius, m_arcRotationStepRadians, ap.rot.GetColumn2(), screenScale)) { m_highlightAxis = AxisZ; GetIEditor()->SetAxisConstraints(AXIS_Z); return true; } return false; } void CRotateTool::DeleteThis() { delete this; } bool CRotateTool::OnKeyDown(CViewport* view, uint32 nChar, uint32 nRepCnt, uint32 nFlags) { if (nChar == VK_ESCAPE) { GetIEditor()->GetObjectManager()->ClearSelection(); return true; } return false; } Matrix34 CRotateTool::GetTransform(RefCoordSys referenceCoordinateSystem, IDisplayViewport* view) { Matrix34 objectTransform = Matrix34::CreateIdentity(); if (m_object) { switch (referenceCoordinateSystem) { case COORDS_VIEW: if (view) { objectTransform = view->GetViewTM(); } objectTransform.SetTranslation(m_object->GetWorldTM().GetTranslation()); break; case COORDS_LOCAL: objectTransform = m_object->GetWorldTM(); break; case COORDS_PARENT: if (m_object->GetParent()) { Matrix34 parentTM = m_object->GetParent()->GetWorldTM(); parentTM.SetTranslation(m_object->GetWorldTM().GetTranslation()); objectTransform = parentTM; } else { objectTransform.SetTranslation(m_object->GetWorldTM().GetTranslation()); } break; case COORDS_WORLD: objectTransform.SetTranslation(m_object->GetWorldTM().GetTranslation()); break; } } return objectTransform; } float CRotateTool::CalculateOrientation(const QPoint& p1, const QPoint& p2, const QPoint& p3) { // Source: https://www.geeksforgeeks.org/orientation-3-ordered-points/ float c = (p2.y() - p1.y()) * (p3.x() - p2.x()) - (p3.y() - p2.y()) * (p2.x() - p1.x()); return c > 0 ? 1.0f : -1.0f; } CRotateTool::~CRotateTool() { if (m_object) { m_object->RemoveEventListener(functor(*this, &CRotateTool::OnObjectEvent)); } GetIEditor()->GetObjectManager()->SetSelectCallback(nullptr); } bool CRotateTool::OnLButtonDown(CViewport* view, int nFlags, const QPoint& p) { QPoint point = p; m_hc.view = view; m_hc.b2DViewport = view->GetType() != ET_ViewportCamera; m_hc.point2d = point; if (nFlags == OBJFLAG_IS_PARTICLE) { view->setHitcontext(point, m_hc.raySrc, m_hc.rayDir); } else { view->ViewToWorldRay(point, m_hc.raySrc, m_hc.rayDir); } if (m_hc.object && m_hc.object != m_object) { GetIEditor()->ClearSelection(); return CObjectMode::OnLButtonDown(view, nFlags, point); } if (m_highlightAxis != AxisNone) { view->BeginUndo(); view->CaptureMouse(); view->SetCurrentCursor(STD_CURSOR_ROTATE); m_draggingMouse = true; // Store the starting drag angle when we first click the mouse, we will need this to know // how much of the rotation we need to apply. if (m_highlightAxis == AxisView) { Vec3 cameraViewDir = m_hc.camera->GetViewdir().GetNormalized(); float cameraAngle = atan2f(cameraViewDir.y, -cameraViewDir.x); m_initialViewAxisAngleRadians = m_angleToCursor - cameraAngle - (g_PI / 2); m_initialViewAxisAngleRadians -= static_cast(g_PI); } m_lastPosition = point; m_rotationAngles = Ang3(0, 0, 0); AzToolsFramework::EntityIdList selectedEntities; AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult( selectedEntities, &AzToolsFramework::ToolsApplicationRequests::Bus::Events::GetSelectedEntities); AzToolsFramework::EditorTransformChangeNotificationBus::Broadcast( &AzToolsFramework::EditorTransformChangeNotificationBus::Events::OnEntityTransformChanging, selectedEntities); return true; } return CObjectMode::OnLButtonDown(view, nFlags, point); } bool CRotateTool::OnLButtonUp(CViewport* view, int nFlags, const QPoint& p) { QPoint point = p; if (nFlags == OBJFLAG_IS_PARTICLE) { view->setHitcontext(point, m_hc.raySrc, m_hc.rayDir); } else { view->ViewToWorldRay(point, m_hc.raySrc, m_hc.rayDir); } if (m_draggingMouse) { // We are no longer dragging the mouse, so we will release it and reset any state variables. { AzToolsFramework::ScopedUndoBatch undo("Rotate"); } view->AcceptUndo("Rotate Selection"); view->ReleaseMouse(); view->SetCurrentCursor(STD_CURSOR_DEFAULT); m_draggingMouse = false; m_totalRotationAngle = 0.f; m_initialViewAxisAngleRadians = 0.f; m_angleToCursor = 0.f; // Apply the transform changes to the selection. if (m_bTransformChanged) { CSelectionGroup* pSelection = GetIEditor()->GetSelection(); if (pSelection) { pSelection->FinishChanges(); } m_bTransformChanged = false; GetIEditor()->GetObjectManager()->GetSelection()->ObjectModified(); view->ResetSelectionRegion(); // Reset selected rectangle. view->SetSelectionRectangle(QRect()); view->SetAxisConstrain(GetIEditor()->GetAxisConstrains()); AzToolsFramework::EntityIdList selectedEntities; AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult( selectedEntities, &AzToolsFramework::ToolsApplicationRequests::Bus::Events::GetSelectedEntities); AzToolsFramework::EditorTransformChangeNotificationBus::Broadcast( &AzToolsFramework::EditorTransformChangeNotificationBus::Events::OnEntityTransformChanged, selectedEntities); } } return CObjectMode::OnLButtonUp(view, nFlags, point); } bool CRotateTool::OnMouseMove(CViewport* view, int nFlags, const QPoint& p) { QPoint point = p; if (!m_object) { return CObjectMode::OnMouseMove(view, nFlags, point); } // Prevent the opening of the context menu during a mouse move. m_openContext = false; // We calculate the mouse drag direction vector's angle from the object to the mouse position. QPoint objectCenter; if (nFlags != OBJFLAG_IS_PARTICLE) { objectCenter = view->WorldToView(GetIEditor()->GetSelection()->GetCenter()); } else if (parent() && parent()->isWidgetType()) { QWidget *wParent = static_cast(parent()); // HACK: This is only valid for the particle editor and needs refactored. const QRect rect = wParent->contentsRect(); objectCenter = view->WorldToViewParticleEditor(m_object->GetWorldPos(), rect.width(), rect.height()); } Vec2 dragDirection = Vec2(point.x() - objectCenter.x(), point.y() - objectCenter.y()); dragDirection.Normalize(); float angleToCursor = (atan2f(dragDirection.y, dragDirection.x)); m_angleToCursor = angleToCursor - g_PI2 * floor(angleToCursor / g_PI2); if (m_draggingMouse) { GetIEditor()->RestoreUndo(); view->SetCurrentCursor(STD_CURSOR_ROTATE); RefCoordSys referenceCoordSys = GetIEditor()->GetReferenceCoordSys(); if (m_highlightAxis == AxisView) { // Calculate the angular difference between the starting rotation angle, taking into account the camera's angle to ensure a smooth rotation. Vec3 cameraViewDir = m_hc.camera->GetViewdir(); float cameraAngle = atan2f(cameraViewDir.y, cameraViewDir.x); float angleDelta = (m_angleToCursor - g_PI2 * floor(m_initialViewAxisAngleRadians / g_PI2)) - (m_initialViewAxisAngleRadians - (cameraAngle - (g_PI / 2))); // Snap the angle is necessary angleDelta = view->GetViewManager()->GetGrid()->SnapAngle(RAD2DEG(angleDelta)); if (nFlags != OBJFLAG_IS_PARTICLE) { Matrix34 viewRotation = Matrix34::CreateRotationAA(DEG2RAD(angleDelta), cameraViewDir); GetIEditor()->GetSelection()->Rotate(viewRotation, COORDS_WORLD); } else { Quat quatRotation = Quat::CreateRotationAA(DEG2RAD(angleDelta), cameraViewDir); m_object->SetRotation(quatRotation); } m_bTransformChanged = true; } else if (m_highlightAxis != AxisNone) { float distanceMoved = (point - m_lastPosition).manhattanLength(); // screen-space distance dragged float distanceToCenter = (m_lastPosition - objectCenter).manhattanLength(); // screen-space distance to object center float roationDelta = RAD2DEG(atan2f(distanceMoved, distanceToCenter)); // unsigned rotation angle float orientation = CalculateOrientation(objectCenter, m_lastPosition, point); // Calculate if rotation dragging gizmo clockwise or counter-clockwise m_lastPosition = point; // Calculate orientation of the object's axis towards camera Vec3 directionToObject = (GetIEditor()->GetSelection()->GetCenter() - m_hc.camera->GetMatrix().GetTranslation()).normalize(); float directionX = 1.0f; float directionY = 1.0f; float directionZ = 1.0f; switch (referenceCoordSys) { case COORDS_LOCAL: directionX = directionToObject.Dot(m_object->GetWorldTM().GetColumn0()) > 0 ? -1.0f : 1.0f; directionY = directionToObject.Dot(m_object->GetWorldTM().GetColumn1()) > 0 ? -1.0f : 1.0f; directionZ = directionToObject.Dot(m_object->GetWorldTM().GetColumn2()) > 0 ? -1.0f : 1.0f; break; case COORDS_PARENT: if (m_object->GetParent()) { directionX = directionToObject.Dot(m_object->GetParent()->GetWorldTM().GetColumn0()) > 0 ? -1.0f : 1.0f; directionY = directionToObject.Dot(m_object->GetParent()->GetWorldTM().GetColumn1()) > 0 ? -1.0f : 1.0f; directionZ = directionToObject.Dot(m_object->GetParent()->GetWorldTM().GetColumn2()) > 0 ? -1.0f : 1.0f; } else { directionX = directionToObject.Dot(m_object->GetWorldTM().GetColumn0()) > 0 ? -1.0f : 1.0f; directionY = directionToObject.Dot(m_object->GetWorldTM().GetColumn1()) > 0 ? -1.0f : 1.0f; directionZ = directionToObject.Dot(m_object->GetWorldTM().GetColumn2()) > 0 ? -1.0f : 1.0f; } break; case COORDS_VIEW: case COORDS_WORLD: directionX = directionToObject.Dot(Vec3(1, 0, 0)) > 0 ? -1.0f : 1.0f; directionY = directionToObject.Dot(Vec3(0, 1, 0)) > 0 ? -1.0f : 1.0f; directionZ = directionToObject.Dot(Vec3(0, 0, 1)) > 0 ? -1.0f : 1.0f; break; } switch (m_highlightAxis) { case AxisX: m_rotationAngles.x += roationDelta * directionX * orientation; break; case AxisY: m_rotationAngles.y += roationDelta * directionY * orientation; break; case AxisZ: m_rotationAngles.z += roationDelta * directionZ * orientation; break; default: break; } // Snap the angle if necessary m_rotationAngles = view->GetViewManager()->GetGrid()->SnapAngle(m_rotationAngles); // Compute the total amount rotated Vec3 vDragValue = Vec3(m_rotationAngles); m_totalRotationAngle = DEG2RAD(vDragValue.len()); // Apply the rotation if (nFlags != OBJFLAG_IS_PARTICLE) { GetIEditor()->GetSelection()->Rotate(m_rotationAngles, referenceCoordSys); } else { Quat currentRotation = (m_object->GetRotation()); Quat rotateTM = currentRotation * Quat::CreateRotationXYZ(DEG2RAD(-m_rotationAngles / 50.0f)); m_object->SetRotation(rotateTM); } m_bTransformChanged = fabs(m_totalRotationAngle) > FLT_EPSILON; } } else { // If we are not yet dragging the mouse, do the hit testing to highlight the axis the mouse is over. m_hc.view = view; m_hc.b2DViewport = view->GetType() != ET_ViewportCamera; m_hc.point2d = point; if (nFlags != OBJFLAG_IS_PARTICLE) { view->ViewToWorldRay(point, m_hc.raySrc, m_hc.rayDir); } else { view->setHitcontext(point, m_hc.raySrc, m_hc.rayDir); } if (HitTest(m_object, m_hc)) { // Display a cursor that makes it clear to the user that he is over an axis that can be rotated. view->SetCurrentCursor(STD_CURSOR_ROTATE); } else { // Nothing has been hit, reset the cursor back to default in case it was changed previously. view->SetCurrentCursor(STD_CURSOR_DEFAULT); } } // We always consider the rotation tool's OnMove event handled return true; } float CRotateTool::GetScreenScale(IDisplayViewport* view, CCamera* camera /*=nullptr*/) { Matrix34 objectTransform = GetTransform(GetIEditor()->GetReferenceCoordSys(), view); AffineParts ap; ap.Decompose(objectTransform); if (m_object && m_object->CheckFlags(OBJFLAG_IS_PARTICLE)) { return view->GetScreenScaleFactor(*camera, ap.pos) * kViewDistanceScaleFactor; } return static_cast(view)->GetScreenScaleFactor(ap.pos) * kViewDistanceScaleFactor; } void CRotateTool::DrawHitTestGeometry(DisplayContext& dc, HitContext& hc) { AffineParts ap; ap.Decompose(GetTransform(GetIEditor()->GetReferenceCoordSys(), dc.view)); Vec3 position = ap.pos; CSelectionGroup* selection = GetIEditor()->GetSelection(); if (selection->GetCount() > 1 && !m_object->CheckFlags(OBJFLAG_IS_PARTICLE)) { position = selection->GetCenter(); } float screenScale = GetScreenScale(dc.view, dc.camera); // Draw debug test surface for each axis. m_axes[AxisX].DebugDrawHitTestSurface(dc, hc, position, m_basisAxisRadius, m_arcRotationStepRadians, ap.rot.GetColumn0(), screenScale); m_axes[AxisY].DebugDrawHitTestSurface(dc, hc, position, m_basisAxisRadius, m_arcRotationStepRadians, ap.rot.GetColumn1(), screenScale); m_axes[AxisZ].DebugDrawHitTestSurface(dc, hc, position, m_basisAxisRadius, m_arcRotationStepRadians, ap.rot.GetColumn2(), screenScale); // We don't render the view axis rotation for multiple selection. if (!hc.b2DViewport && selection->GetCount() == 1) { Vec3 cameraViewDir = hc.view->GetViewTM().GetColumn1().GetNormalized(); m_axes[AxisView].DebugDrawHitTestSurface(dc, hc, position, m_viewAxisRadius, m_arcRotationStepRadians, cameraViewDir, screenScale); } } void CRotateTool::DrawViewDirectionAngleTracking(DisplayContext& dc, HitContext& hc) { Vec3 a; Vec3 b; // Calculate a basis for the camera view direction. Vec3 cameraViewDir = hc.view->GetViewTM().GetColumn1().GetNormalized(); GetBasisVectors(cameraViewDir, a, b); // Calculates the camera view direction angle. float angle = m_angleToCursor; float cameraAngle = atan2f(cameraViewDir.y, -cameraViewDir.x); // Ensures the angle remains camera aligned. angle -= cameraAngle - (g_PI / 2); // The position will be either the object's center or the selection's center. Vec3 position = GetTransform(GetIEditor()->GetReferenceCoordSys(), dc.view).GetTranslation(); CSelectionGroup* selection = GetIEditor()->GetSelection(); if (selection->GetCount() > 1 && !m_object->CheckFlags(OBJFLAG_IS_PARTICLE)) { position = selection->GetCenter(); } float screenScale = GetScreenScale(dc.view, dc.camera); const float cosAngle = cos(angle); const float sinAngle = sin(angle); // The resulting position will be in a circular orientation based on the resulting angle. Vec3 p0; p0.x = position.x + (cosAngle * a.x + sinAngle * b.x) * m_viewAxisRadius * screenScale; p0.y = position.y + (cosAngle * a.y + sinAngle * b.y) * m_viewAxisRadius * screenScale; p0.z = position.z + (cosAngle * a.z + sinAngle * b.z) * m_viewAxisRadius * screenScale; const float ballRadius = 0.1f * screenScale; dc.SetColor(Col_Magenta); dc.DrawBall(p0, ballRadius); } namespace RotationDrawHelper { Axis::Axis(const ColorF& defaultColor, const ColorF& highlightColor) { m_colors[StateDefault] = defaultColor; m_colors[StateHighlight] = highlightColor; } void Axis::Draw(DisplayContext& dc, const Vec3& position, const Vec3& axis, float angleRadians, float angleStepRadians, float radius, bool highlighted, CBaseObject* object, float screenScale) { if (static_cast(dc.view)->GetType() != ET_ViewportCamera || object->CheckFlags(OBJFLAG_IS_PARTICLE)) { bool set = dc.SetDrawInFrontMode(true); // Draw the front facing arc dc.SetColor(!highlighted ? m_colors[StateDefault] : m_colors[StateHighlight]); dc.DrawArc(position, radius * screenScale, 0.f, 360.f, RAD2DEG(angleStepRadians), axis); dc.SetDrawInFrontMode(set); } else { // Draw the front facing arc dc.SetColor(!highlighted ? m_colors[StateDefault] : m_colors[StateHighlight]); dc.DrawArc(position, radius * screenScale, RAD2DEG(angleRadians) - 90.f, 180.f, RAD2DEG(angleStepRadians), axis); // Draw the back side dc.SetColor(!highlighted ? Col_Gray : m_colors[StateHighlight]); dc.DrawArc(position, radius * screenScale, RAD2DEG(angleRadians) + 90.f, 180.f, RAD2DEG(angleStepRadians), axis); } static bool drawAxisMidPoint = false; if (drawAxisMidPoint) { const float kBallRadius = 0.085f; Vec3 a; Vec3 b; GetBasisVectors(axis, a, b); float cosAngle = cos(angleRadians); float sinAngle = sin(angleRadians); Vec3 offset; offset.x = position.x + (cosAngle * a.x + sinAngle * b.x) * screenScale * radius; offset.y = position.y + (cosAngle * a.y + sinAngle * b.y) * screenScale * radius; offset.z = position.z + (cosAngle * a.z + sinAngle * b.z) * screenScale * radius; dc.SetColor(!highlighted ? m_colors[StateDefault] : m_colors[StateHighlight]); dc.DrawBall(offset, kBallRadius * screenScale); } } void Axis::GenerateHitTestGeometry(HitContext& hc, const Vec3& position, float radius, float angleStepRadians, const Vec3& axis, float screenScale) { m_vertices.clear(); // The number of vertices relies on the angleStepRadians, the smaller the angle, the higher the vertex count. int numVertices = static_cast(std::ceil(g_PI2 / angleStepRadians)); Vec3 a; Vec3 b; GetBasisVectors(axis, a, b); // The geometry is calculated by computing a circle aligned to the specified axis. float angle = 0.f; for (int i = 0; i < numVertices; ++i) { float cosAngle = cos(angle); float sinAngle = sin(angle); Vec3 p; p.x = position.x + (cosAngle * a.x + sinAngle * b.x) * radius * screenScale; p.y = position.y + (cosAngle * a.y + sinAngle * b.y) * radius * screenScale; p.z = position.z + (cosAngle * a.z + sinAngle * b.z) * radius * screenScale; m_vertices.push_back(p); angle += angleStepRadians; } } bool Axis::IntersectRayWithQuad(const Ray& ray, Vec3 quad[4], Vec3& contact) { contact = Vec3(); // Tests ray vs. two quads, the front facing quad and a back facing quad. // will return true if an intersection occurs and the world space position of the contact. return (Intersect::Ray_Triangle(ray, quad[0], quad[1], quad[2], contact) || Intersect::Ray_Triangle(ray, quad[0], quad[2], quad[3], contact) || Intersect::Ray_Triangle(ray, quad[0], quad[2], quad[1], contact) || Intersect::Ray_Triangle(ray, quad[0], quad[3], quad[2], contact)); } bool Axis::HitTest(CBaseObject* object, HitContext& hc, float radius, float angleStepRadians, const Vec3& axis, float screenScale) { AffineParts ap; ap.Decompose(object->GetWorldTM()); Vec3 position = ap.pos; CSelectionGroup* selection = GetIEditor()->GetSelection(); if (selection->GetCount() > 1 && !object->CheckFlags(OBJFLAG_IS_PARTICLE)) { position = selection->GetCenter(); } // Generate intersection testing geometry GenerateHitTestGeometry(hc, position, radius, angleStepRadians, axis, screenScale); Ray ray; ray.origin = hc.raySrc; ray.direction = hc.rayDir; // Calculate the face normal with the first two vertices in the intersection geometry. Vec3 vdir0 = (m_vertices[0] - m_vertices[1]).GetNormalized(); Vec3 vdir1 = (m_vertices[2] - m_vertices[1]).GetNormalized(); Vec3 normal; if (!hc.b2DViewport) { normal = hc.view->GetViewTM().GetColumn1(); } else { normal = hc.view->GetConstructionPlane()->n; } float shortestDistance = std::numeric_limits::max(); size_t numVertices = m_vertices.size(); for (size_t i = 0; i < numVertices; ++i) { const Vec3& v0 = m_vertices[i]; const Vec3& v1 = m_vertices[(i + 1) % numVertices]; Vec3 right = (v0 - v1).Cross(normal).GetNormalized() * screenScale * m_hitTestWidth; // Calculates the quad vertices aligned to the face normal. Vec3 quad[4]; quad[0] = v0 + right; quad[1] = v1 + right; quad[2] = v1 - right; quad[3] = v0 - right; Vec3 contact; if (IntersectRayWithQuad(ray, quad, contact)) { Vec3 intersectionPoint; if (PointToLineDistance(v0, v1, contact, intersectionPoint)) { // Ensure the intersection is within the quad's extents float distanceToIntersection = intersectionPoint.GetDistance(contact); if (distanceToIntersection < shortestDistance) { shortestDistance = distanceToIntersection; } } } } // if shortestDistance is less than the maximum possible distance, we have an intersection. if (shortestDistance < std::numeric_limits::max() - FLT_EPSILON) { hc.object = object; hc.dist = shortestDistance; return true; } return false; } void Axis::DebugDrawHitTestSurface(DisplayContext& dc, HitContext& hc, const Vec3& position, float radius, float angleStepRadians, const Vec3& axis, float screenScale) { // Generate the geometry for rendering. GenerateHitTestGeometry(hc, position, radius, angleStepRadians, axis, screenScale); // Calculate the face normal with the first two vertices in the intersection geometry. Vec3 vdir0 = (m_vertices[0] - m_vertices[1]).GetNormalized(); Vec3 vdir1 = (m_vertices[2] - m_vertices[1]).GetNormalized(); Vec3 normal; if (!hc.b2DViewport) { normal = hc.view->GetViewTM().GetColumn1(); } else { normal = hc.view->GetConstructionPlane()->n; } float shortestDistance = std::numeric_limits::max(); Ray ray; ray.origin = hc.raySrc; ray.direction = hc.rayDir; size_t numVertices = m_vertices.size(); for (size_t i = 0; i < numVertices; ++i) { const Vec3& v0 = m_vertices[i]; const Vec3& v1 = m_vertices[(i + 1) % numVertices]; Vec3 right = (v0 - v1).Cross(normal).GetNormalized() * screenScale * m_hitTestWidth; // Calculates the quad vertices aligned to the face normal. Vec3 quad[4]; quad[0] = v0 + right; quad[1] = v1 + right; quad[2] = v1 - right; quad[3] = v0 - right; // Draw double sided quad to ensure it is always visible regardless of camera orientation. dc.DrawQuad(quad[0], quad[1], quad[2], quad[3]); dc.DrawQuad(quad[3], quad[2], quad[1], quad[0]); Vec3 contact; if (IntersectRayWithQuad(ray, quad, contact)) { Vec3 intersectionPoint; if (PointToLineDistance(v0, v1, contact, intersectionPoint)) { // Ensure the intersection is within the quad's extents float distanceToIntersection = intersectionPoint.GetDistance(contact); if (distanceToIntersection < shortestDistance) { shortestDistance = distanceToIntersection; // Highlight the quad at which an intersection occurred. auto c = dc.GetColor(); dc.SetColor(Col_Red); dc.DrawQuad(quad[0], quad[1], quad[2], quad[3]); dc.DrawQuad(quad[3], quad[2], quad[1], quad[0]); dc.SetColor(c); } } } } } namespace AngleDecorator { void Draw(DisplayContext& dc, const Vec3& position, const Vec3& axisToAlign, float startAngleRadians, float sweepAngleRadians, float stepAngleRadians, float radius, float screenScale) { float angle = startAngleRadians; if (fabs(sweepAngleRadians) < FLT_EPSILON || sweepAngleRadians < stepAngleRadians) { return; } if (sweepAngleRadians > g_PI) { sweepAngleRadians = g_PI - (fabs(sweepAngleRadians - g_PI)); stepAngleRadians = -stepAngleRadians; } Vec3 a; Vec3 b; GetBasisVectors(axisToAlign, a, b); float cosAngle = cos(angle); float sinAngle = sin(angle); // Pre-calculate the first vertex, this is useful for rendering the first handle ball. Vec3 p0; p0.x = position.x + (cosAngle * a.x + sinAngle * b.x) * radius * screenScale; p0.y = position.y + (cosAngle * a.y + sinAngle * b.y) * radius * screenScale; p0.z = position.z + (cosAngle * a.z + sinAngle * b.z) * radius * screenScale; const float ballRadius = 0.1f * screenScale; // TODO: colors should be configurable properties dc.SetColor(0.f, 1.f, 0.f, 1.f); dc.DrawBall(p0, ballRadius); float alpha = 0.5f; dc.SetColor(0.8f, 0.8f, 0.8f, 0.5f); // Number of vertices is defined by stepAngleRadians, the smaller the step the higher vertex count. int numVertices = static_cast(fabs(sweepAngleRadians / stepAngleRadians)); if (numVertices >= 2) { Vec3 p1; for (int i = 0; i < numVertices; ++i) { // We pre-calculated the first vertex, so we can advance the angle angle += stepAngleRadians; const float cosAngle = cos(angle); const float sinAngle = sin(angle); p1.x = position.x + (cosAngle * a.x + sinAngle * b.x) * radius * screenScale; p1.y = position.y + (cosAngle * a.y + sinAngle * b.y) * radius * screenScale; p1.z = position.z + (cosAngle * a.z + sinAngle * b.z) * radius * screenScale; // Draws a triangle from the object's position to p0 and p1. dc.SetColor(0.8f, 0.8f, 0.8f, alpha); dc.DrawTri(position, p0, p1); alpha += 0.5f * (i / numVertices); p0 = p1; } // Draw the end handle ball. dc.SetColor(1.f, 0.f, 0.f, 1.f); dc.DrawBall(p1, ballRadius); } } } } RotationControlConfiguration::RotationControlConfiguration() { DefineConstIntCVar(RotationControl_DrawDecorators, 0, VF_NULL, "Toggles the display of the angular decorator."); DefineConstIntCVar(RotationControl_DebugHitTesting, 0, VF_NULL, "Renders the hit testing geometry used for mouse input control."); DefineConstIntCVar(RotationControl_AngleTracking, 0, VF_NULL, "Displays a sphere aligned to the mouse cursor direction for debugging."); } #include