diff --git a/src/GraphControl/Control/Grapher.cpp b/src/GraphControl/Control/Grapher.cpp index 3a434cb..9cd33ca 100644 --- a/src/GraphControl/Control/Grapher.cpp +++ b/src/GraphControl/Control/Grapher.cpp @@ -7,8 +7,10 @@ using namespace GraphControl::DX; using namespace Platform; using namespace Platform::Collections; using namespace std; +using namespace Windows::Devices::Input; using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; +using namespace Windows::System; using namespace Windows::UI; using namespace Windows::UI::Input; using namespace Windows::UI::Xaml; @@ -16,23 +18,33 @@ using namespace Windows::UI::Xaml::Controls; using namespace Windows::UI::Xaml::Input; using namespace Windows::UI::Xaml::Media; -namespace GraphControl +namespace { constexpr auto s_defaultStyleKey = L"GraphControl.Grapher"; constexpr auto s_templateKey_SwapChainPanel = L"GraphSurface"; - DependencyProperty^ Grapher::s_equationTemplateProperty; constexpr auto s_propertyName_EquationTemplate = L"EquationTemplate"; - - DependencyProperty^ Grapher::s_equationsProperty; constexpr auto s_propertyName_Equations = L"Equations"; - - DependencyProperty^ Grapher::s_equationsSourceProperty; constexpr auto s_propertyName_EquationsSource = L"EquationsSource"; - - DependencyProperty^ Grapher::s_forceProportionalAxesTemplateProperty; constexpr auto s_propertyName_ForceProportionalAxes = L"ForceProportionalAxes"; + // Helper function for converting a pointer position to a position that the graphing engine will understand. + // posX/posY are the pointer position elements and width,height are the dimensions of the graph surface. + // The graphing engine interprets x,y position between the range [-1, 1]. + // Translate the pointer position to the [-1, 1] bounds. + __inline pair PointerPositionToGraphPosition(double posX, double posY, double width, double height) + { + return make_pair(( 2 * posX / width - 1 ), ( 1 - 2 * posY / height )); + } +} + +namespace GraphControl +{ + DependencyProperty^ Grapher::s_equationTemplateProperty; + DependencyProperty^ Grapher::s_equationsProperty; + DependencyProperty^ Grapher::s_equationsSourceProperty; + DependencyProperty^ Grapher::s_forceProportionalAxesTemplateProperty; + Grapher::Grapher() : m_solver{ IMathSolver::CreateMathSolver() } , m_graph{ m_solver->CreateGrapher() } @@ -45,6 +57,13 @@ namespace GraphControl this->Loaded += ref new RoutedEventHandler(this, &Grapher::OnLoaded); this->Unloaded += ref new RoutedEventHandler(this, &Grapher::OnUnloaded); + + this->ManipulationMode = + ManipulationModes::TranslateX | + ManipulationModes::TranslateY | + ManipulationModes::TranslateInertia | + ManipulationModes::Scale | + ManipulationModes::ScaleInertia; } void Grapher::OnLoaded(Object^ sender, RoutedEventArgs^ args) @@ -66,6 +85,20 @@ namespace GraphControl } } + void Grapher::ScaleRange(double centerX, double centerY, double scale) + { + if (m_graph != nullptr && m_renderMain != nullptr) + { + if (auto renderer = m_graph->GetRenderer()) + { + if (SUCCEEDED(renderer->ScaleRange(centerX, centerY, scale))) + { + m_renderMain->RunRenderPass(); + } + } + } + } + void Grapher::OnApplyTemplate() { auto swapChainPanel = dynamic_cast(GetTemplateChild(StringReference(s_templateKey_SwapChainPanel))); @@ -389,4 +422,106 @@ namespace GraphControl e->Handled = true; } } + + void Grapher::OnPointerWheelChanged(PointerRoutedEventArgs^ e) + { + PointerPoint^ currentPointer = e->GetCurrentPoint(/*relative to*/ this); + + double delta = currentPointer->Properties->MouseWheelDelta; + + // The maximum delta is 120 according to: + // https://docs.microsoft.com/en-us/uwp/api/windows.ui.input.pointerpointproperties.mousewheeldelta#Windows_UI_Input_PointerPointProperties_MouseWheelDelta + // Apply a dampening effect so that small mouse movements have a smoother zoom. + constexpr double scrollDamper = 0.15; + double scale = 1.0 + (abs(delta) / WHEEL_DELTA) * scrollDamper; + + // positive delta if wheel scrolled away from the user + if (delta >= 0) + { + scale = 1.0 / scale; + } + + // For scaling, the graphing engine interprets x,y position between the range [-1, 1]. + // Translate the pointer position to the [-1, 1] bounds. + const auto& pos = currentPointer->Position; + const auto[centerX, centerY] = PointerPositionToGraphPosition(pos.X, pos.Y, ActualWidth, ActualHeight); + + ScaleRange(centerX, centerY, scale); + + e->Handled = true; + } + + void Grapher::OnPointerPressed(PointerRoutedEventArgs^ e) + { + // Set the pointer capture to the element being interacted with so that only it + // will fire pointer-related events + CapturePointer(e->Pointer); + } + + void Grapher::OnPointerReleased(PointerRoutedEventArgs^ e) + { + ReleasePointerCapture(e->Pointer); + } + + void Grapher::OnPointerCanceled(PointerRoutedEventArgs^ e) + { + ReleasePointerCapture(e->Pointer); + } + + void Grapher::OnManipulationDelta(ManipulationDeltaRoutedEventArgs^ e) + { + if (m_renderMain != nullptr && m_graph != nullptr) + { + if (auto renderer = m_graph->GetRenderer()) + { + // Only call for a render pass if we actually scaled or translated. + bool needsRenderPass = false; + + const double width = ActualWidth; + const double height = ActualHeight; + + const auto& translation = e->Delta.Translation; + double translationX = translation.X; + double translationY = translation.Y; + if (translationX != 0 || translationY != 0) + { + // The graphing engine pans the graph according to a ratio for x and y. + // A value of +1 means move a half screen in the positive direction for the given axis. + // Convert the manipulation's translation values to ratios for the engine. + translationX /= -width; + translationY /= height; + + if (FAILED(renderer->MoveRangeByRatio(translationX, translationY))) + { + return; + } + + needsRenderPass = true; + } + + if (double scale = e->Delta.Scale; scale != 1.0) + { + // The graphing engine interprets scale amounts as the inverse of the value retrieved + // from the ManipulationUpdatedEventArgs. Invert the scale amount for the engine. + scale = 1.0 / scale; + + // Convert from PointerPosition to graph position. + const auto& pos = e->Position; + const auto[centerX, centerY] = PointerPositionToGraphPosition(pos.X, pos.Y, width, height); + + if (FAILED(renderer->ScaleRange(centerX, centerY, scale))) + { + return; + } + + needsRenderPass = true; + } + + if (needsRenderPass) + { + m_renderMain->RunRenderPass(); + } + } + } + } } diff --git a/src/GraphControl/Control/Grapher.h b/src/GraphControl/Control/Grapher.h index b969aed..42901a5 100644 --- a/src/GraphControl/Control/Grapher.h +++ b/src/GraphControl/Control/Grapher.h @@ -107,6 +107,11 @@ namespace GraphControl void OnPointerEntered(Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) override; void OnPointerMoved(Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) override; void OnPointerExited(Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) override; + void OnPointerWheelChanged(Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) override; + void OnPointerPressed(Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) override; + void OnPointerReleased(Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) override; + void OnPointerCanceled(Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) override; + void OnManipulationDelta(Windows::UI::Xaml::Input::ManipulationDeltaRoutedEventArgs^ e) override; #pragma endregion private: @@ -122,6 +127,7 @@ namespace GraphControl void OnDataSourceChanged(GraphControl::InspectingDataSource^ sender, GraphControl::DataSourceChangedEventArgs args); void OnEquationsChanged(Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ args); + void OnEquationsVectorChanged(Windows::Foundation::Collections::IObservableVector ^sender, Windows::Foundation::Collections::IVectorChangedEventArgs ^event); void OnEquationChanged(); void UpdateGraph(); @@ -136,6 +142,8 @@ namespace GraphControl void OnItemsAdded(int index, int count); void OnItemsRemoved(int index, int count); + void ScaleRange(double centerX, double centerY, double scale); + private: DX::RenderMain^ m_renderMain = nullptr; @@ -155,6 +163,5 @@ namespace GraphControl const std::unique_ptr m_solver; const std::shared_ptr m_graph; - void OnEquationsVectorChanged(Windows::Foundation::Collections::IObservableVector ^sender, Windows::Foundation::Collections::IVectorChangedEventArgs ^event); -}; + }; } diff --git a/src/GraphingInterfaces/GraphingEnums.h b/src/GraphingInterfaces/GraphingEnums.h index 47139ad..0686d74 100644 --- a/src/GraphingInterfaces/GraphingEnums.h +++ b/src/GraphingInterfaces/GraphingEnums.h @@ -326,24 +326,18 @@ namespace Graphing // Zoom out on all axes by the predefined ratio ZoomOut, - // Zoom out on X axis only, leave the range of Y (and Z in 3D) unchanged + // Zoom out on X axis only, leave the range of Y unchanged WidenX, - // Zoom in on X axis only, leave the range of Y (and Z in 3D) unchanged + // Zoom in on X axis only, leave the range of Y unchanged ShrinkX, - // Zoom out on Y axis only, leave the range of X (and Z in 3D) unchanged + // Zoom out on Y axis only, leave the range of X unchanged WidenY, - // Zoom in on Y axis only, leave the range of X (and Z in 3D) unchanged + // Zoom in on Y axis only, leave the range of X unchanged ShrinkY, - // Zoom out on Z axis only, leave the range of X and Y unchanged. Apply to 3D graph only but not 2D graph. - WidenZ, - - // Zoom in on Z axis only, leave the range of X and Y unchanged. Apply to 3D graph only but not 2D graph. - ShrinkZ, - // Move the view window of the graph towards the negative X axis. MoveNegativeX, @@ -356,12 +350,6 @@ namespace Graphing // Move the view window of the graph towards the positive Y axis. MovePositiveY, - // Move the view window of the graph towards the negative Z axis. - MoveNegativeZ, - - // Move the view window of the graph towards the positive Z axis. - MovePositiveZ, - // Zoom in on all axes by the predefined ratio. The ratio is smaller than used in ZoomIn result in a smoother motion SmoothZoomIn, diff --git a/src/GraphingInterfaces/IGraphRenderer.h b/src/GraphingInterfaces/IGraphRenderer.h index a3b6770..85af9fe 100644 --- a/src/GraphingInterfaces/IGraphRenderer.h +++ b/src/GraphingInterfaces/IGraphRenderer.h @@ -16,5 +16,10 @@ namespace Graphing::Renderer virtual HRESULT DrawD2D1(ID2D1Factory* pDirect2dFactory, ID2D1RenderTarget* pRenderTarget, bool& hasSomeMissingDataOut) = 0; virtual HRESULT GetClosePointData(float inScreenPointX, float inScreenPointY, int& formulaIdOut, float& xScreenPointOut, float& yScreenPointOut, float& xValueOut, float& yValueOut) = 0; + + virtual HRESULT ScaleRange(double centerX, double centerY, double scale) = 0; + virtual HRESULT ChangeRange(ChangeRangeAction action) = 0; + virtual HRESULT MoveRangeByRatio(double ratioX, double ratioY) = 0; + virtual HRESULT ResetRange() = 0; }; }