gesture detector fixes

This commit is contained in:
Nick Fisher
2024-09-19 09:19:23 +08:00
parent 31d31dd583
commit 242b2f6faa
5 changed files with 146 additions and 91 deletions

View File

@@ -14,16 +14,19 @@ class DefaultZoomCameraDelegate {
DefaultZoomCameraDelegate(this.viewer, DefaultZoomCameraDelegate(this.viewer,
{this.zoomSensitivity = 0.005, this.getDistanceToTarget}); {this.zoomSensitivity = 0.005, this.getDistanceToTarget});
double calculateZoomDistance(double scrollDelta, Vector2? velocity, Vector3 cameraPosition) { ///
double? distanceToTarget = getDistanceToTarget?.call(cameraPosition); /// Converts the given [scrollDelta] (usually somewhere between 1 and -1) to
double zoomDistance = scrollDelta * zoomSensitivity; /// a percentage of the current camera distance (either to the origin,
if (distanceToTarget != null) { /// or to a custom target) along its forward vector.
zoomDistance *= distanceToTarget; /// In other words, "shift "
if (zoomDistance.abs() < 0.0001) { ///
zoomDistance = scrollDelta * zoomSensitivity; double calculateZoomFactor(
} double scrollDelta, Vector2? velocity) {
double zoomFactor = scrollDelta * zoomSensitivity;
if (zoomFactor.abs() < 0.0001) {
zoomFactor = scrollDelta * zoomSensitivity;
} }
return max(zoomDistance, scrollDelta * zoomSensitivity); return zoomFactor;
} }
@override @override
@@ -36,7 +39,7 @@ class DefaultZoomCameraDelegate {
forwardVector.normalize(); forwardVector.normalize();
var zoomDistance = var zoomDistance =
calculateZoomDistance(scrollDelta, velocity, cameraPosition); calculateZoomFactor(scrollDelta, velocity);
Vector3 newPosition = cameraPosition + (forwardVector * zoomDistance); Vector3 newPosition = cameraPosition + (forwardVector * zoomDistance);
await viewer.setCameraPosition(newPosition.x, newPosition.y, newPosition.z); await viewer.setCameraPosition(newPosition.x, newPosition.y, newPosition.z);

View File

@@ -9,6 +9,7 @@ import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.d
import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/fixed_orbit_camera_rotation_delegate.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/fixed_orbit_camera_rotation_delegate.dart';
import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/free_flight_camera_delegate.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/free_flight_camera_delegate.dart';
import 'package:thermion_flutter/thermion_flutter.dart'; import 'package:thermion_flutter/thermion_flutter.dart';
import 'package:vector_math/vector_math_64.dart';
class DelegateGestureHandler implements ThermionGestureHandler { class DelegateGestureHandler implements ThermionGestureHandler {
final ThermionViewer viewer; final ThermionViewer viewer;
@@ -48,10 +49,16 @@ class DelegateGestureHandler implements ThermionGestureHandler {
_initializeAccumulatedDeltas(); _initializeAccumulatedDeltas();
} }
factory DelegateGestureHandler.fixedOrbit(ThermionViewer viewer) => factory DelegateGestureHandler.fixedOrbit(ThermionViewer viewer,
{double? Function(Vector3)? getDistanceToTarget,
double rotationSensitivity = 0.001,
double zoomSensitivity = 0.001}) =>
DelegateGestureHandler( DelegateGestureHandler(
viewer: viewer, viewer: viewer,
cameraDelegate: FixedOrbitRotateCameraDelegate(viewer), cameraDelegate: FixedOrbitRotateCameraDelegate(viewer,
getDistanceToTarget: getDistanceToTarget,
rotationSensitivity: rotationSensitivity,
zoomSensitivity: zoomSensitivity),
velocityDelegate: DefaultVelocityDelegate(), velocityDelegate: DefaultVelocityDelegate(),
); );
@@ -96,7 +103,8 @@ class DelegateGestureHandler implements ThermionGestureHandler {
// Do nothing // Do nothing
break; break;
default: default:
_logger.warning("Unsupported gesture action: $action for type: $gestureType"); _logger.warning(
"Unsupported gesture action: $action for type: $gestureType");
break; break;
} }
@@ -105,12 +113,13 @@ class DelegateGestureHandler implements ThermionGestureHandler {
} }
if (_accumulatedScrollDelta != 0.0) { if (_accumulatedScrollDelta != 0.0) {
await cameraDelegate?.zoom(_accumulatedScrollDelta, velocityDelegate?.velocity); await cameraDelegate?.zoom(
_accumulatedScrollDelta, velocityDelegate?.velocity);
_accumulatedScrollDelta = 0.0; _accumulatedScrollDelta = 0.0;
} }
} }
@override @override
Future<void> onPointerDown(Offset localPosition, int buttons) async { Future<void> onPointerDown(Offset localPosition, int buttons) async {
velocityDelegate?.stopDeceleration(); velocityDelegate?.stopDeceleration();
_activePointers++; _activePointers++;
@@ -120,13 +129,18 @@ class DelegateGestureHandler implements ThermionGestureHandler {
} }
@override @override
Future<void> onPointerMove(Offset localPosition, Offset delta, int buttons) async { Future<void> onPointerMove(
Offset localPosition, Offset delta, int buttons) async {
GestureType gestureType = _getGestureTypeFromButtons(buttons); GestureType gestureType = _getGestureTypeFromButtons(buttons);
if (gestureType == GestureType.MMB_HOLD_AND_MOVE || if (gestureType == GestureType.MMB_HOLD_AND_MOVE ||
(_actions[GestureType.POINTER_MOVE] == GestureAction.ROTATE_CAMERA && gestureType == GestureType.POINTER_MOVE)) { (_actions[GestureType.POINTER_MOVE] == GestureAction.ROTATE_CAMERA &&
_accumulatedDeltas[GestureType.MMB_HOLD_AND_MOVE] = (_accumulatedDeltas[GestureType.MMB_HOLD_AND_MOVE] ?? Offset.zero) + delta; gestureType == GestureType.POINTER_MOVE)) {
_accumulatedDeltas[GestureType.MMB_HOLD_AND_MOVE] =
(_accumulatedDeltas[GestureType.MMB_HOLD_AND_MOVE] ?? Offset.zero) +
delta;
} else { } else {
_accumulatedDeltas[gestureType] = (_accumulatedDeltas[gestureType] ?? Offset.zero) + delta; _accumulatedDeltas[gestureType] =
(_accumulatedDeltas[gestureType] ?? Offset.zero) + delta;
} }
} }
@@ -142,22 +156,26 @@ class DelegateGestureHandler implements ThermionGestureHandler {
} }
GestureType _getGestureTypeFromButtons(int buttons) { GestureType _getGestureTypeFromButtons(int buttons) {
if (buttons & kPrimaryMouseButton != 0) return GestureType.LMB_HOLD_AND_MOVE; if (buttons & kPrimaryMouseButton != 0)
if (buttons & kMiddleMouseButton != 0 || _isMiddleMouseButtonPressed) return GestureType.MMB_HOLD_AND_MOVE; return GestureType.LMB_HOLD_AND_MOVE;
if (buttons & kMiddleMouseButton != 0 || _isMiddleMouseButtonPressed)
return GestureType.MMB_HOLD_AND_MOVE;
return GestureType.POINTER_MOVE; return GestureType.POINTER_MOVE;
} }
@override @override
Future<void> onPointerHover(Offset localPosition, Offset delta) async { Future<void> onPointerHover(Offset localPosition, Offset delta) async {
if (_actions[GestureType.POINTER_MOVE] == GestureAction.ROTATE_CAMERA) { if (_actions[GestureType.POINTER_MOVE] == GestureAction.ROTATE_CAMERA) {
_accumulatedDeltas[GestureType.POINTER_MOVE] = (_accumulatedDeltas[GestureType.POINTER_MOVE] ?? Offset.zero) + delta; _accumulatedDeltas[GestureType.POINTER_MOVE] =
(_accumulatedDeltas[GestureType.POINTER_MOVE] ?? Offset.zero) + delta;
} }
} }
@override @override
Future<void> onPointerScroll(Offset localPosition, double scrollDelta) async { Future<void> onPointerScroll(Offset localPosition, double scrollDelta) async {
if (_actions[GestureType.SCROLLWHEEL] != GestureAction.ZOOM_CAMERA) { if (_actions[GestureType.SCROLLWHEEL] != GestureAction.ZOOM_CAMERA) {
throw Exception("Unsupported action: ${_actions[GestureType.SCROLLWHEEL]}"); throw Exception(
"Unsupported action: ${_actions[GestureType.SCROLLWHEEL]}");
} }
try { try {
@@ -203,6 +221,9 @@ class DelegateGestureHandler implements ThermionGestureHandler {
} }
bool _handleKeyEvent(KeyEvent event) { bool _handleKeyEvent(KeyEvent event) {
if (_actions[GestureType.KEYDOWN] == GestureAction.NONE) {
return false;
}
if (event is KeyDownEvent || event is KeyRepeatEvent) { if (event is KeyDownEvent || event is KeyRepeatEvent) {
cameraDelegate?.onKeypress(event.physicalKey); cameraDelegate?.onKeypress(event.physicalKey);
return true; return true;
@@ -212,4 +233,4 @@ class DelegateGestureHandler implements ThermionGestureHandler {
} }
return false; return false;
} }
} }

View File

@@ -5,7 +5,7 @@ import 'package:vector_math/vector_math_64.dart';
abstract class CameraDelegate { abstract class CameraDelegate {
Future<void> rotate(Offset delta, Vector2? velocity); Future<void> rotate(Offset delta, Vector2? velocity);
Future<void> pan(Offset delta, Vector2? velocity); Future<void> pan(Offset delta, Vector2? velocity);
Future<void> zoom(double scrollDelta, Vector2? velocity); Future<void> zoom(double yScrollDeltaInPixels, Vector2? velocity);
Future<void> onKeypress(PhysicalKeyboardKey key); Future<void> onKeypress(PhysicalKeyboardKey key);
Future<void> onKeyRelease(PhysicalKeyboardKey key); Future<void> onKeyRelease(PhysicalKeyboardKey key);
} }

View File

@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/src/services/keyboard_key.g.dart'; import 'package:flutter/src/services/keyboard_key.g.dart';
@@ -10,21 +11,30 @@ import 'package:vector_math/vector_math_64.dart';
class FixedOrbitRotateCameraDelegate implements CameraDelegate { class FixedOrbitRotateCameraDelegate implements CameraDelegate {
final ThermionViewer viewer; final ThermionViewer viewer;
static final _up = Vector3(0, 1, 0);
static final _forward = Vector3(0, 0, -1);
static final Vector3 _right = Vector3(1, 0, 0);
static const double _rotationSensitivity = 0.01; double rotationSensitivity = 0.01;
late DefaultZoomCameraDelegate _zoomCameraDelegate; late DefaultZoomCameraDelegate _zoomCameraDelegate;
Offset _accumulatedRotationDelta = Offset.zero; Offset _accumulatedRotationDelta = Offset.zero;
double _accumulatedZoomDelta = 0.0; double _accumulatedZoomDelta = 0.0;
static final _up = Vector3(0, 1, 0);
Timer? _updateTimer; Timer? _updateTimer;
FixedOrbitRotateCameraDelegate(this.viewer) { Vector3 _targetPosition = Vector3(0, 0, 0);
_zoomCameraDelegate = DefaultZoomCameraDelegate(this.viewer);
double? Function(Vector3)? getDistanceToTarget;
FixedOrbitRotateCameraDelegate(this.viewer,
{this.getDistanceToTarget,
double? rotationSensitivity,
double zoomSensitivity = 0.005}) {
_zoomCameraDelegate = DefaultZoomCameraDelegate(this.viewer,
zoomSensitivity: zoomSensitivity,
getDistanceToTarget: getDistanceToTarget);
this.rotationSensitivity = rotationSensitivity ?? 0.01;
_startUpdateTimer(); _startUpdateTimer();
} }
@@ -49,64 +59,88 @@ class FixedOrbitRotateCameraDelegate implements CameraDelegate {
} }
@override @override
Future<void> zoom(double scrollDelta, Vector2? velocity) async { Future<void> zoom(double yScrollDeltaInPixels, Vector2? velocity) async {
_accumulatedZoomDelta += scrollDelta; if (yScrollDeltaInPixels > 1) {
_accumulatedZoomDelta++;
} else {
_accumulatedZoomDelta--;
}
} }
Future<void> _applyAccumulatedUpdates() async { Future<void> _applyAccumulatedUpdates() async {
if (_accumulatedRotationDelta != Offset.zero || _accumulatedZoomDelta != 0.0) { if (_accumulatedRotationDelta.distanceSquared == 0.0 &&
Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); _accumulatedZoomDelta == 0.0) {
Vector3 currentPosition = currentModelMatrix.getTranslation(); return;
double distance = currentPosition.length;
Quaternion currentRotation =
Quaternion.fromRotation(currentModelMatrix.getRotation());
// Apply rotation
if (_accumulatedRotationDelta != Offset.zero) {
double deltaX = _accumulatedRotationDelta.dx * _rotationSensitivity * viewer.pixelRatio;
double deltaY = _accumulatedRotationDelta.dy * _rotationSensitivity * viewer.pixelRatio;
Quaternion yawRotation = Quaternion.axisAngle(_up, -deltaX);
Quaternion pitchRotation = Quaternion.axisAngle(_right, -deltaY);
currentRotation = currentRotation * yawRotation * pitchRotation;
currentRotation.normalize();
_accumulatedRotationDelta = Offset.zero;
}
// Apply zoom
if (_accumulatedZoomDelta != 0.0) {
var zoomDistance = _zoomCameraDelegate.calculateZoomDistance(
_accumulatedZoomDelta,
null,
Vector3.zero()
);
distance += zoomDistance;
distance = distance.clamp(0.1, 1000.0); // Adjust these limits as needed
_accumulatedZoomDelta = 0.0;
}
// Calculate new position
Vector3 newPosition = _forward.clone()
..applyQuaternion(currentRotation)
..scale(-distance);
// Create and set new model matrix
Matrix4 newModelMatrix =
Matrix4.compose(newPosition, currentRotation, Vector3(1, 1, 1));
await viewer.setCameraModelMatrix4(newModelMatrix);
} }
var modelMatrix = await viewer.getCameraModelMatrix();
Vector3 cameraPosition = modelMatrix.getTranslation();
final heightAboveSurface = getDistanceToTarget?.call(cameraPosition) ?? 1.0;
final sphereRadius = cameraPosition.length - heightAboveSurface;
// Apply rotation
if (_accumulatedRotationDelta.distanceSquared > 0) {
// Calculate the distance factor
final distanceFactor = sqrt((heightAboveSurface / sphereRadius) + 1);
// Adjust the base angle per meter
final baseAnglePerMeter = 10000 / sphereRadius;
final adjustedAnglePerMeter = baseAnglePerMeter * distanceFactor;
final metersOnSurface = _accumulatedRotationDelta;
final rotationX = metersOnSurface.dy * adjustedAnglePerMeter;
final rotationY = metersOnSurface.dx * adjustedAnglePerMeter;
Matrix4 rotation = Matrix4.rotationX(rotationX)..rotateY(rotationY);
Vector3 newPos = rotation.getRotation() * cameraPosition;
cameraPosition = newPos;
}
// Normalize the position to maintain constant distance from center
cameraPosition =
cameraPosition.normalized() * (sphereRadius + heightAboveSurface);
// Apply zoom (modified to ensure minimum 10m distance)
if (_accumulatedZoomDelta != 0.0) {
var zoomFactor = -0.5 * _accumulatedZoomDelta;
double newHeight = heightAboveSurface * (1 - zoomFactor);
newHeight = newHeight.clamp(
10.0, double.infinity); // Prevent getting closer than 10m to surface
cameraPosition = cameraPosition.normalized() * (sphereRadius + newHeight);
_accumulatedZoomDelta = 0.0;
}
// Ensure minimum 10m distance even after rotation
final currentHeight = cameraPosition.length - sphereRadius;
if (currentHeight < 10.0) {
cameraPosition = cameraPosition.normalized() * (sphereRadius + 10.0);
}
// Calculate view matrix (unchanged)
Vector3 forward = cameraPosition.normalized();
Vector3 up = Vector3(0, 1, 0);
final right = up.cross(forward)..normalize();
up = forward.cross(right);
Matrix4 viewMatrix = makeViewMatrix(cameraPosition, Vector3.zero(), up);
viewMatrix.invert();
// Set the camera model matrix
await viewer.setCameraModelMatrix4(viewMatrix);
_accumulatedRotationDelta = Offset.zero;
} }
@override @override
Future<void> onKeyRelease(PhysicalKeyboardKey key) async { Future<void> onKeyRelease(PhysicalKeyboardKey key) async {
//ignore // Ignore
} }
@override @override
Future<void> onKeypress(PhysicalKeyboardKey key) async { Future<void> onKeypress(PhysicalKeyboardKey key) async {
//ignore // Ignore
} }
} }

View File

@@ -9,8 +9,6 @@ import 'package:vector_math/vector_math_64.dart';
class FreeFlightCameraDelegate implements CameraDelegate { class FreeFlightCameraDelegate implements CameraDelegate {
final ThermionViewer viewer; final ThermionViewer viewer;
final bool lockPitch;
final bool lockYaw;
final bool lockRoll; final bool lockRoll;
final Vector3? minBounds; final Vector3? minBounds;
final Vector3? maxBounds; final Vector3? maxBounds;
@@ -36,8 +34,7 @@ class FreeFlightCameraDelegate implements CameraDelegate {
FreeFlightCameraDelegate( FreeFlightCameraDelegate(
this.viewer, { this.viewer, {
this.lockPitch = false,
this.lockYaw = false,
this.lockRoll = false, this.lockRoll = false,
this.minBounds, this.minBounds,
this.maxBounds, this.maxBounds,
@@ -76,9 +73,9 @@ class FreeFlightCameraDelegate implements CameraDelegate {
// Apply rotation // Apply rotation
if (_accumulatedRotation != Offset.zero) { if (_accumulatedRotation != Offset.zero) {
double deltaX = lockYaw ? 0 : _accumulatedRotation.dx * rotationSensitivity * viewer.pixelRatio; double deltaX = _accumulatedRotation.dx * rotationSensitivity * viewer.pixelRatio;
double deltaY = lockPitch ? 0 : _accumulatedRotation.dy * rotationSensitivity * viewer.pixelRatio; double deltaY = _accumulatedRotation.dy * rotationSensitivity * viewer.pixelRatio;
double deltaZ = lockRoll ? 0 : (_accumulatedRotation.dx + _accumulatedRotation.dy) * rotationSensitivity * 0.5 * viewer.pixelRatio; double deltaZ = (_accumulatedRotation.dx + _accumulatedRotation.dy) * rotationSensitivity * 0.5 * viewer.pixelRatio;
Quaternion yawRotation = Quaternion.axisAngle(_up, -deltaX); Quaternion yawRotation = Quaternion.axisAngle(_up, -deltaX);
Quaternion pitchRotation = Quaternion.axisAngle(_right, -deltaY); Quaternion pitchRotation = Quaternion.axisAngle(_right, -deltaY);
@@ -150,7 +147,7 @@ class FreeFlightCameraDelegate implements CameraDelegate {
@override @override
Future<void> zoom(double scrollDelta, Vector2? velocity) async { Future<void> zoom(double scrollDelta, Vector2? velocity) async {
_accumulatedZoom += scrollDelta; _accumulatedZoom -= scrollDelta;
_lastVelocity = velocity; _lastVelocity = velocity;
} }