From c93952abfd3551af75d04703ac23321b9f86d094 Mon Sep 17 00:00:00 2001 From: paulhoux Date: Wed, 3 Jan 2024 15:13:13 +0100 Subject: [PATCH 1/2] Adds CanvasUi to cinder. This is a 2D equivalent of CameraUi. It allows users to easily add pan and zoom to their 2D applications. CanvasUi is header-only. --- include/cinder/CanvasUi.h | 224 +++++++++++++++++++++++++++++++++++++ proj/vc2019/cinder.vcxproj | 1 + 2 files changed, 225 insertions(+) create mode 100644 include/cinder/CanvasUi.h diff --git a/include/cinder/CanvasUi.h b/include/cinder/CanvasUi.h new file mode 100644 index 0000000000..e31bb2b6ec --- /dev/null +++ b/include/cinder/CanvasUi.h @@ -0,0 +1,224 @@ +/* +Copyright (c) 2016-2021, Paul Houx Creative Coding - All rights reserved. +This code is intended for use with the Cinder C++ library: http://libcinder.org + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and + the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "cinder/app/MouseEvent.h" +#include "cinder/app/Window.h" + +namespace cinder { + +//! Enables user interaction with a 2D canvas via the mouse. +class CanvasUi { + public: + CanvasUi() = default; + ~CanvasUi() { disconnect(); } + + CanvasUi( const CanvasUi &rhs ) { *this = rhs; } + CanvasUi( CanvasUi &&rhs ) noexcept = delete; + CanvasUi &operator=( const CanvasUi &rhs ) + { + if( this != &rhs ) { + mScale = rhs.mScale; + mMouse = rhs.mMouse; + mClick = rhs.mClick; + mAnchor = rhs.mAnchor; + mPosition = rhs.mPosition; + mOriginal = rhs.mOriginal; + mModelMatrix = rhs.mModelMatrix; + mIsDirty = rhs.mIsDirty; + mMouseWheelMultiplier = rhs.mMouseWheelMultiplier; + mWindow = rhs.mWindow; + mSignalPriority = rhs.mSignalPriority; + mEnabled = rhs.mEnabled; + connect( mWindow, mSignalPriority ); + } + return *this; + } + CanvasUi &operator=( CanvasUi &&rhs ) noexcept = delete; + + //! Connects to mouseDown, mouseDrag and mouseWheel signals of \a window, with optional priority \a signalPriority. + void connect( const app::WindowRef &window, int signalPriority = 0 ) + { + if( window == mWindow && signalPriority == mSignalPriority ) + return; + + disconnect(); + + mWindow = window; + mWindowSize = window->getSize(); + mSignalPriority = signalPriority; + if( window ) { + mConnections.push_back( window->getSignalDraw().connect( signalPriority, [this]() { update(); } ) ); + mConnections.push_back( window->getSignalMouseDown().connect( signalPriority, [this]( app::MouseEvent &event ) { mouseDown( event ); } ) ); + mConnections.push_back( window->getSignalMouseDrag().connect( signalPriority, [this]( app::MouseEvent &event ) { mouseDrag( event ); } ) ); + mConnections.push_back( window->getSignalMouseWheel().connect( signalPriority, [this]( app::MouseEvent &event ) { mouseWheel( event ); } ) ); + } + } + //! Disconnects all signal handlers. + void disconnect() + { + for( auto &conn : mConnections ) + conn.disconnect(); + mConnections.clear(); + mWindow.reset(); + } + //! Returns whether the CanvasUi is connected to mouse and window signal handlers. + bool isConnected() const { return mWindow != nullptr; } + //! Sets whether the CanvasUi will modify its transform matrix either through its Window signals or through the various mouse*() member functions. + void enable( bool enable = true ) { mEnabled = enable; } + //! Prevents the CanvasUi from modifying its transform matrix either through its Window signals or through the various mouse*() member functions. + void disable() { mEnabled = false; } + //! Returns whether the CanvasUi will modify its transform matrix either through its Window signals or through the various mouse*() member functions. + bool isEnabled() const { return mEnabled; } + //! Returns the current position and scale as a transform matrix. + const mat4 &getModelMatrix() const + { + if( mIsDirty ) { + // Update model matrix. + mModelMatrix = translate( vec3( mPosition, 0 ) ); + mModelMatrix *= scale( vec3( mScale ) ); + mModelMatrix *= translate( vec3( -mAnchor, 0 ) ); + mIsInvDirty = true; + mIsDirty = false; + } + + return mModelMatrix; + } + //! Returns the inverse of the current transform matrix. Can be used to convert coordinates. See also `CanvasUi::toLocal`. + const mat4 &getInverseModelMatrix() const + { + if( mIsInvDirty ) { + mInvModelMatrix = inverse( mModelMatrix ); + mIsInvDirty = false; + } + + return mInvModelMatrix; + } + //! Converts a given point \a pt from world to object space, effectively undoing the canvas transformations. + vec2 toLocal( const vec2 &pt ) const + { + auto &m = getInverseModelMatrix(); + return { m * vec4( pt, 0, 1 ) }; + } + + void reset() + { + mPosition = mAnchor = vec2( 0 ); + mScale = mScaleTarget = 1.0f; + mIsDirty = mIsInvDirty = true; + } + + void resize( const ivec2 &size ) + { + mPosition += 0.5f * vec2( size - mWindowSize ) / mScaleTarget; + mAnchor += 0.5f * vec2( size - mWindowSize ) / mScaleTarget; + mIsDirty = mIsInvDirty = true; + mWindowSize = size; + } + + void mouseDown( app::MouseEvent &event ) + { + if( event.isRightDown() ) + reset(); + + mouseDown( event.getPos() ); + } + void mouseDrag( app::MouseEvent &event ) { mouseDrag( event.getPos() ); } + + void mouseWheel( app::MouseEvent &event ) { mouseWheel( event.getPos(), event.getWheelIncrement() ); } + + void mouseDown( const vec2 &mousePos ) + { + if( !mEnabled ) + return; + + reposition( mousePos ); + + mClick = mousePos; + mOriginal = mPosition; + } + + void mouseDrag( const vec2 &mousePos ) + { + if( !mEnabled ) + return; + + mMouse = mousePos; + mPosition = mOriginal + mMouse - mClick; + mIsDirty = true; + } + + void mouseWheel( const vec2 &mousePos, float increment ) + { + if( !mEnabled ) + return; + + reposition( mousePos ); + + mMouse = mousePos; + mScaleTarget *= 1.0f + mMouseWheelMultiplier * increment; + mScaleTarget = clamp( mScaleTarget, 0.1f, 100.0f ); // Limit scale to the range [10%...10,000%]. + mIsDirty = true; + } + + //! Sets the multiplier on mouse wheel zooming. Larger values zoom faster. Negative values invert the direction. Default is \c 0.1. + void setMouseWheelMultiplier( float multiplier ) { mMouseWheelMultiplier = multiplier; } + //! Returns the multiplier on mouse wheel zooming. Default is \c 0.1. + float getMouseWheelMultiplier() const { return mMouseWheelMultiplier; } + + private: + void update() + { + mScale += mMouseWheelMultiplier * ( mScaleTarget - mScale ); + mIsDirty = mIsInvDirty = true; + } + void reposition( const vec2 &mouse ) + { + // Convert mouse to object space. + vec2 anchor = vec2( getInverseModelMatrix() * vec4( mouse, 0, 1 ) ); + + // Calculate new position, anchor and scale. + mPosition += vec2( getModelMatrix() * vec4( anchor - mAnchor, 0, 0 ) ); + mAnchor = anchor; + } + + std::vector mConnections; + app::WindowRef mWindow; + ivec2 mWindowSize; + vec2 mMouse{ 0 }; + vec2 mClick{ 0 }; + vec2 mAnchor{ 0 }; + vec2 mPosition{ 0 }; + vec2 mOriginal{ 0 }; + float mScale{ 1 }; + float mScaleTarget{ 1 }; + float mMouseWheelMultiplier{ 0.1f }; + int mSignalPriority{ 0 }; + mutable mat4 mModelMatrix; + mutable mat4 mInvModelMatrix; + mutable bool mIsDirty{ false }; + mutable bool mIsInvDirty{ false }; + bool mEnabled{ true }; +}; + +} // namespace cinder \ No newline at end of file diff --git a/proj/vc2019/cinder.vcxproj b/proj/vc2019/cinder.vcxproj index e1cba7652e..484c47a421 100644 --- a/proj/vc2019/cinder.vcxproj +++ b/proj/vc2019/cinder.vcxproj @@ -1071,6 +1071,7 @@ + From 006186b7206402cbfb5a87a6642b99ccdaeabd9b Mon Sep 17 00:00:00 2001 From: paulhoux Date: Wed, 3 Jan 2024 15:36:41 +0100 Subject: [PATCH 2/2] Tweaked CanvasUi. --- include/cinder/CanvasUi.h | 46 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/include/cinder/CanvasUi.h b/include/cinder/CanvasUi.h index e31bb2b6ec..c593024b40 100644 --- a/include/cinder/CanvasUi.h +++ b/include/cinder/CanvasUi.h @@ -39,13 +39,12 @@ class CanvasUi { { if( this != &rhs ) { mScale = rhs.mScale; - mMouse = rhs.mMouse; - mClick = rhs.mClick; + mMouseDownPos = rhs.mMouseDownPos; mAnchor = rhs.mAnchor; mPosition = rhs.mPosition; mOriginal = rhs.mOriginal; mModelMatrix = rhs.mModelMatrix; - mIsDirty = rhs.mIsDirty; + mModelCached = rhs.mModelCached; mMouseWheelMultiplier = rhs.mMouseWheelMultiplier; mWindow = rhs.mWindow; mSignalPriority = rhs.mSignalPriority; @@ -93,13 +92,13 @@ class CanvasUi { //! Returns the current position and scale as a transform matrix. const mat4 &getModelMatrix() const { - if( mIsDirty ) { + if( !mModelCached ) { // Update model matrix. mModelMatrix = translate( vec3( mPosition, 0 ) ); mModelMatrix *= scale( vec3( mScale ) ); mModelMatrix *= translate( vec3( -mAnchor, 0 ) ); - mIsInvDirty = true; - mIsDirty = false; + mModelCached = true; + mInvModelCached = false; } return mModelMatrix; @@ -107,9 +106,9 @@ class CanvasUi { //! Returns the inverse of the current transform matrix. Can be used to convert coordinates. See also `CanvasUi::toLocal`. const mat4 &getInverseModelMatrix() const { - if( mIsInvDirty ) { - mInvModelMatrix = inverse( mModelMatrix ); - mIsInvDirty = false; + if( !mModelCached || !mInvModelCached ) { + mInvModelMatrix = inverse( getModelMatrix() ); + mInvModelCached = true; } return mInvModelMatrix; @@ -117,7 +116,7 @@ class CanvasUi { //! Converts a given point \a pt from world to object space, effectively undoing the canvas transformations. vec2 toLocal( const vec2 &pt ) const { - auto &m = getInverseModelMatrix(); + const auto &m = getInverseModelMatrix(); return { m * vec4( pt, 0, 1 ) }; } @@ -125,14 +124,14 @@ class CanvasUi { { mPosition = mAnchor = vec2( 0 ); mScale = mScaleTarget = 1.0f; - mIsDirty = mIsInvDirty = true; + mModelCached = mInvModelCached = false; } void resize( const ivec2 &size ) { mPosition += 0.5f * vec2( size - mWindowSize ) / mScaleTarget; mAnchor += 0.5f * vec2( size - mWindowSize ) / mScaleTarget; - mIsDirty = mIsInvDirty = true; + mModelCached = mInvModelCached = false; mWindowSize = size; } @@ -154,7 +153,7 @@ class CanvasUi { reposition( mousePos ); - mClick = mousePos; + mMouseDownPos = mousePos; mOriginal = mPosition; } @@ -163,9 +162,8 @@ class CanvasUi { if( !mEnabled ) return; - mMouse = mousePos; - mPosition = mOriginal + mMouse - mClick; - mIsDirty = true; + mPosition = mOriginal + mousePos - mMouseDownPos; + mModelCached = mInvModelCached = false; } void mouseWheel( const vec2 &mousePos, float increment ) @@ -175,10 +173,8 @@ class CanvasUi { reposition( mousePos ); - mMouse = mousePos; mScaleTarget *= 1.0f + mMouseWheelMultiplier * increment; mScaleTarget = clamp( mScaleTarget, 0.1f, 100.0f ); // Limit scale to the range [10%...10,000%]. - mIsDirty = true; } //! Sets the multiplier on mouse wheel zooming. Larger values zoom faster. Negative values invert the direction. Default is \c 0.1. @@ -189,9 +185,12 @@ class CanvasUi { private: void update() { - mScale += mMouseWheelMultiplier * ( mScaleTarget - mScale ); - mIsDirty = mIsInvDirty = true; + if( !approxEqual( mScaleTarget, mScale ) ) { + mScale += 0.1f * ( mScaleTarget - mScale ); + mModelCached = mInvModelCached = false; + } } + void reposition( const vec2 &mouse ) { // Convert mouse to object space. @@ -205,8 +204,7 @@ class CanvasUi { std::vector mConnections; app::WindowRef mWindow; ivec2 mWindowSize; - vec2 mMouse{ 0 }; - vec2 mClick{ 0 }; + vec2 mMouseDownPos{ 0 }; vec2 mAnchor{ 0 }; vec2 mPosition{ 0 }; vec2 mOriginal{ 0 }; @@ -216,8 +214,8 @@ class CanvasUi { int mSignalPriority{ 0 }; mutable mat4 mModelMatrix; mutable mat4 mInvModelMatrix; - mutable bool mIsDirty{ false }; - mutable bool mIsInvDirty{ false }; + mutable bool mModelCached{ true }; + mutable bool mInvModelCached{ true }; bool mEnabled{ true }; };