diff --git a/src/rocky/vsg/MapManipulator.cpp b/src/rocky/vsg/MapManipulator.cpp index 95e059c46..685f82593 100644 --- a/src/rocky/vsg/MapManipulator.cpp +++ b/src/rocky/vsg/MapManipulator.cpp @@ -458,6 +458,7 @@ MapManipulator::reinitialize() _thrown = false; _delta.set(0.0, 0.0); _throwDelta.set(0.0, 0.0); + _zoomInertia = ZoomVelocityState(); _continuousDelta.set(0.0, 0.0); _continuous = 0; _lastAction = ACTION_NULL; @@ -465,6 +466,41 @@ MapManipulator::reinitialize() clearEvents(); } +void +MapManipulator::cancelZoomInertia() +{ + _zoomInertia.velocity = 0.0; + _zoomInertia.isActive = false; +} + +bool +MapManipulator::serviceZoomInertia(vsg::time_point now) +{ + if (!_zoomInertia.isActive) + return false; + + double dt = to_seconds(now - _previousTime); + if (dt <= 0.0) + return _zoomInertia.isActive; + + // Apply velocity as zoom delta + double frameDelta = _zoomInertia.velocity * dt; + zoom(0.0, frameDelta); + + // Frame-rate independent exponential decay + double decayFactor = std::pow(settings.scrollZoomFriction, dt * 60.0); + _zoomInertia.velocity *= decayFactor; + + // Stop when below threshold + if (std::abs(_zoomInertia.velocity) < settings.scrollZoomThreshold) + { + _zoomInertia.velocity = 0.0; + _zoomInertia.isActive = false; + } + + return _zoomInertia.isActive; +} + bool MapManipulator::createLocalCoordFrame(const vsg::dvec3& worldPos, vsg::dmat4& out_frame) const { @@ -999,6 +1035,9 @@ MapManipulator::apply(vsg::ButtonPressEvent& buttonPress) //std::cout << "ButtonPressEvent" << std::endl; + // Cancel any active zoom inertia + cancelZoomInertia(); + // simply record the button press event. clearEvents(); @@ -1155,6 +1194,15 @@ MapManipulator::apply(vsg::FrameEvent& frame) serviceTask(frame.time); + // Service zoom inertia + if (_zoomInertia.isActive) + { + if (serviceZoomInertia(frame.time)) + { + _dirty = true; + } + } + if (isSettingViewpoint()) { setViewpointFrame(frame.time); @@ -1884,7 +1932,28 @@ MapManipulator::handleAction( case ACTION_ZOOM: case ACTION_ZOOM_IN: case ACTION_ZOOM_OUT: - _task.set(TASK_ZOOM, d, duration, time); + if (settings.scrollZoomEnabled) + { + // Velocity-based zoom + double direction = (action._dir == DIR_UP) ? -1.0 : 1.0; + _zoomInertia.velocity += direction * settings.scrollZoomAcceleration * settings.scrollSensitivity; + + // Clamp to max velocity + if (std::abs(_zoomInertia.velocity) > settings.maxScrollZoomVelocity) + { + _zoomInertia.velocity = ((_zoomInertia.velocity > 0) ? 1.0 : -1.0) * settings.maxScrollZoomVelocity; + } + + _zoomInertia.isActive = true; + _dirty = true; + if (_context) + _context->requestFrame(); + } + else + { + // Legacy task-based zoom + _task.set(TASK_ZOOM, d, duration, time); + } break; default: diff --git a/src/rocky/vsg/MapManipulator.h b/src/rocky/vsg/MapManipulator.h index 7b25ccc09..346e24ce2 100644 --- a/src/rocky/vsg/MapManipulator.h +++ b/src/rocky/vsg/MapManipulator.h @@ -289,6 +289,21 @@ namespace ROCKY_NAMESPACE //! Whtehr to zoom towards the mouse cursor when zooming bool zoomToMouse = true; + //! Enable velocity-based scroll zoom (vs discrete task-based) + bool scrollZoomEnabled = true; + + //! Acceleration per scroll event (zoom units per scroll notch) + double scrollZoomAcceleration = 0.3; + + //! Friction coefficient - velocity retained per 1/60s frame (0.88 = 12% decay) + double scrollZoomFriction = 0.88; + + //! Minimum velocity to keep animating + double scrollZoomThreshold = 0.001; + + //! Maximum zoom velocity cap (prevents jarring zoom from rapid scrolling) + double maxScrollZoomVelocity = 3.0; + //! Assigns behavior to the action of dragging the mouse while depressing one or //! more mouse buttons and modifier keys. @@ -430,6 +445,12 @@ namespace ROCKY_NAMESPACE //vsg::time_point _previousTick; }; + //! State for velocity-based scroll zoom + struct ZoomVelocityState { + double velocity = 0.0; + bool isActive = false; + }; + // "ticks" the resident Task, which allows for multi-frame animation of navigation // movements. bool serviceTask(vsg::time_point); @@ -472,6 +493,12 @@ namespace ROCKY_NAMESPACE virtual bool handlePointAction(const Action& type, float mx, float my, vsg::time_point time); virtual void handleMovementAction(const ActionType& type, vsg::dvec2 delta); + //! Services the zoom velocity animation each frame + bool serviceZoomInertia(vsg::time_point now); + + //! Cancels any active zoom inertia + void cancelZoomInertia(); + void clearEvents(); vsg::ref_ptr getMapNode() const; @@ -526,6 +553,7 @@ namespace ROCKY_NAMESPACE bool _thrown; vsg::dvec2 _throwDelta; vsg::dvec2 _delta; + ZoomVelocityState _zoomInertia; vsg::dmat4 _viewMatrix; State _state; Task _task;