Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions src/rocky/vsg/MapManipulator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -458,13 +458,114 @@ MapManipulator::reinitialize()
_thrown = false;
_delta.set(0.0, 0.0);
_throwDelta.set(0.0, 0.0);
_throwVelocity.set(0.0, 0.0);
_dragHistory.clear();
_continuousDelta.set(0.0, 0.0);
_continuous = 0;
_lastAction = ACTION_NULL;
_previousMove.clear();
clearEvents();
}

void
MapManipulator::recordDragSample(vsg::time_point time, const vsg::dvec2& ndcDelta)
{
// Add new sample
_dragHistory.push_back({ time, ndcDelta });

// Remove samples older than throwHistoryTime
double historyWindow = settings.throwHistoryTime;
while (!_dragHistory.empty())
{
double age = to_seconds(time - _dragHistory.front().time);
if (age > historyWindow)
_dragHistory.pop_front();
else
break;
}
}

vsg::dvec2
MapManipulator::calculateThrowVelocity(vsg::time_point releaseTime) const
{
if (_dragHistory.size() < 2)
return vsg::dvec2(0.0, 0.0);

// Sum all deltas in the history window
vsg::dvec2 totalDelta(0.0, 0.0);

for (const auto& sample : _dragHistory)
{
totalDelta += sample.ndcDelta;
}

// Calculate time span of the history
double timeSpan = to_seconds(releaseTime - _dragHistory.front().time);

if (timeSpan < 0.001) // Avoid division by near-zero
return vsg::dvec2(0.0, 0.0);

// Velocity = total displacement / time
vsg::dvec2 velocity = totalDelta / timeSpan;

// Clamp to maximum velocity
double speed = vsg::length(velocity);
if (speed > settings.maxThrowVelocity)
{
velocity = vsg::normalize(velocity) * settings.maxThrowVelocity;
}

return velocity;
}

void
MapManipulator::clearDragHistory()
{
_dragHistory.clear();
}

void
MapManipulator::cancelThrow()
{
_thrown = false;
_throwVelocity.set(0.0, 0.0);
}

bool
MapManipulator::serviceThrow(vsg::time_point now)
{
if (!_thrown)
return false;

double dt = to_seconds(now - _previousTime);
if (dt <= 0.0)
return _thrown;

// Apply velocity as pan delta
// Velocity is in NDC/second, so multiply by dt to get this frame's delta
vsg::dvec2 frameDelta = _throwVelocity * dt;
pan(frameDelta.x, frameDelta.y);

// Frame-rate independent exponential decay:
// For decay per reference frame (1/60s): velocity *= decayRate^(dt * 60)
// This ensures consistent behavior regardless of framerate
double referenceFrameRate = 60.0;
double decayExponent = dt * referenceFrameRate;
double decayFactor = std::pow(settings.throwDecayRate, decayExponent);

_throwVelocity *= decayFactor;

// Check if velocity has dropped below threshold
double speed = vsg::length(_throwVelocity);
if (speed < settings.throwThreshold)
{
_thrown = false;
_throwVelocity.set(0.0, 0.0);
}

return _thrown;
}

bool
MapManipulator::createLocalCoordFrame(const vsg::dvec3& worldPos, vsg::dmat4& out_frame) const
{
Expand Down Expand Up @@ -966,6 +1067,9 @@ MapManipulator::apply(vsg::KeyPressEvent& keyPress)
if (keyPress.handled || !withinRenderArea(_previousMove))
return;

// Cancel any active throw on new input
cancelThrow();

_keyPress = keyPress;

recalculateCenterAndDistanceFromLookVector();
Expand Down Expand Up @@ -999,6 +1103,12 @@ MapManipulator::apply(vsg::ButtonPressEvent& buttonPress)

//std::cout << "ButtonPressEvent" << std::endl;

// Cancel any active throw on new input
cancelThrow();

// Clear drag history for fresh velocity calculation
clearDragHistory();

// simply record the button press event.
clearEvents();

Expand Down Expand Up @@ -1027,8 +1137,32 @@ MapManipulator::apply(vsg::ButtonReleaseEvent& buttonRelease)
vsg::dvec3 world;
viewportToWorld(buttonRelease.x, buttonRelease.y, world);
}
else
{
// Not a click - check for throw initiation
// Initiate throw if:
// 1. Throwing is enabled
// 2. Last action was pan
// 3. Not in continuous mode
// 4. Velocity exceeds threshold
if (settings.throwingEnabled &&
_lastAction._type == ACTION_PAN &&
_continuous == 0)
{
vsg::dvec2 velocity = calculateThrowVelocity(buttonRelease.time);
double speed = vsg::length(velocity);

if (speed >= settings.throwThreshold)
{
_thrown = true;
_throwVelocity = velocity;
_dirty = true;
}
}
}

clearEvents();
clearDragHistory();

buttonRelease.handled = true;
}
Expand Down Expand Up @@ -1072,6 +1206,18 @@ MapManipulator::apply(vsg::MoveEvent& moveEvent)
if (handleMouseAction(_lastAction, _previousMove.value(), moveEvent))
_dirty = true;

// Record drag sample for throw velocity calculation (only for pan actions)
if (settings.throwingEnabled &&
(_lastAction._type == ACTION_PAN) &&
_previousMove.has_value())
{
auto curr = ndc(moveEvent);
auto prev = ndc(_previousMove.value());
vsg::dvec2 ndcDelta(curr.x - prev.x, -(curr.y - prev.y));
ndcDelta *= settings.mouseSensitivity;
recordDragSample(moveEvent.time, ndcDelta);
}

if (_continuous > 0) // && !wasContinuous)
{
_continuousAction = _lastAction;
Expand All @@ -1091,6 +1237,9 @@ MapManipulator::apply(vsg::ScrollWheelEvent& scrollEvent)
if (scrollEvent.handled || !withinRenderArea(_previousMove))
return;

// Cancel any active throw on new input
cancelThrow();

//std::cout << "ScrollWheelEvent" << std::endl;

Direction dir =
Expand Down Expand Up @@ -1155,6 +1304,15 @@ MapManipulator::apply(vsg::FrameEvent& frame)

serviceTask(frame.time);

// Service throw animation
if (_thrown)
{
if (serviceThrow(frame.time))
{
_dirty = true; // Keep rendering while throw is active
}
}

if (isSettingViewpoint())
{
setViewpointFrame(frame.time);
Expand Down
39 changes: 39 additions & 0 deletions src/rocky/vsg/MapManipulator.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <rocky/GeoPoint.h>
#include <rocky/Math.h>
#include <rocky/Viewpoint.h>
#include <deque>
#include <optional>
#include <vector>

Expand Down Expand Up @@ -289,6 +290,21 @@ namespace ROCKY_NAMESPACE
//! Whtehr to zoom towards the mouse cursor when zooming
bool zoomToMouse = true;

//! Enable throw/fling after drag release
bool throwingEnabled = true;

//! Friction coefficient - velocity retained per 1/60s frame (0.95 = 5% decay)
double throwDecayRate = 0.95;

//! Minimum velocity (NDC/s) to initiate throw
double throwThreshold = 0.05;

//! History window for velocity calculation (seconds)
double throwHistoryTime = 0.15;

//! Maximum throw velocity cap (NDC/s)
double maxThrowVelocity = 4.0;


//! Assigns behavior to the action of dragging the mouse while depressing one or
//! more mouse buttons and modifier keys.
Expand Down Expand Up @@ -430,6 +446,12 @@ namespace ROCKY_NAMESPACE
//vsg::time_point _previousTick;
};

//! A single sample of drag movement for throw velocity calculation
struct DragSample {
vsg::time_point time;
vsg::dvec2 ndcDelta;
};

// "ticks" the resident Task, which allows for multi-frame animation of navigation
// movements.
bool serviceTask(vsg::time_point);
Expand Down Expand Up @@ -472,6 +494,21 @@ 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);

//! Adds a drag sample to the history buffer (called during mouse/touch drag)
void recordDragSample(vsg::time_point time, const vsg::dvec2& ndcDelta);

//! Calculates throw velocity from recent drag history
vsg::dvec2 calculateThrowVelocity(vsg::time_point releaseTime) const;

//! Clears drag history (called on button press)
void clearDragHistory();

//! Services the throw animation each frame (returns true if throw is still active)
bool serviceThrow(vsg::time_point now);

//! Cancels any active throw
void cancelThrow();

void clearEvents();
vsg::ref_ptr<MapNode> getMapNode() const;

Expand Down Expand Up @@ -525,6 +562,8 @@ namespace ROCKY_NAMESPACE

bool _thrown;
vsg::dvec2 _throwDelta;
vsg::dvec2 _throwVelocity; // Current throw velocity (NDC/second)
std::deque<DragSample> _dragHistory; // Recent drag samples for velocity calculation
vsg::dvec2 _delta;
vsg::dmat4 _viewMatrix;
State _state;
Expand Down