diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_zoom_camera_delegate.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_zoom_camera_delegate.dart index 30c8cd64..a8571173 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_zoom_camera_delegate.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_zoom_camera_delegate.dart @@ -14,16 +14,19 @@ class DefaultZoomCameraDelegate { DefaultZoomCameraDelegate(this.viewer, {this.zoomSensitivity = 0.005, this.getDistanceToTarget}); - double calculateZoomDistance(double scrollDelta, Vector2? velocity, Vector3 cameraPosition) { - double? distanceToTarget = getDistanceToTarget?.call(cameraPosition); - double zoomDistance = scrollDelta * zoomSensitivity; - if (distanceToTarget != null) { - zoomDistance *= distanceToTarget; - if (zoomDistance.abs() < 0.0001) { - zoomDistance = scrollDelta * zoomSensitivity; - } + /// + /// Converts the given [scrollDelta] (usually somewhere between 1 and -1) to + /// a percentage of the current camera distance (either to the origin, + /// or to a custom target) along its forward vector. + /// In other words, "shift " + /// + 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 @@ -36,7 +39,7 @@ class DefaultZoomCameraDelegate { forwardVector.normalize(); var zoomDistance = - calculateZoomDistance(scrollDelta, velocity, cameraPosition); + calculateZoomFactor(scrollDelta, velocity); Vector3 newPosition = cameraPosition + (forwardVector * zoomDistance); await viewer.setCameraPosition(newPosition.x, newPosition.y, newPosition.z); diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegate_gesture_handler.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegate_gesture_handler.dart index 142b73aa..8e898e3a 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegate_gesture_handler.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegate_gesture_handler.dart @@ -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/free_flight_camera_delegate.dart'; import 'package:thermion_flutter/thermion_flutter.dart'; +import 'package:vector_math/vector_math_64.dart'; class DelegateGestureHandler implements ThermionGestureHandler { final ThermionViewer viewer; @@ -48,10 +49,16 @@ class DelegateGestureHandler implements ThermionGestureHandler { _initializeAccumulatedDeltas(); } - factory DelegateGestureHandler.fixedOrbit(ThermionViewer viewer) => + factory DelegateGestureHandler.fixedOrbit(ThermionViewer viewer, + {double? Function(Vector3)? getDistanceToTarget, + double rotationSensitivity = 0.001, + double zoomSensitivity = 0.001}) => DelegateGestureHandler( viewer: viewer, - cameraDelegate: FixedOrbitRotateCameraDelegate(viewer), + cameraDelegate: FixedOrbitRotateCameraDelegate(viewer, + getDistanceToTarget: getDistanceToTarget, + rotationSensitivity: rotationSensitivity, + zoomSensitivity: zoomSensitivity), velocityDelegate: DefaultVelocityDelegate(), ); @@ -96,7 +103,8 @@ class DelegateGestureHandler implements ThermionGestureHandler { // Do nothing break; default: - _logger.warning("Unsupported gesture action: $action for type: $gestureType"); + _logger.warning( + "Unsupported gesture action: $action for type: $gestureType"); break; } @@ -105,12 +113,13 @@ class DelegateGestureHandler implements ThermionGestureHandler { } if (_accumulatedScrollDelta != 0.0) { - await cameraDelegate?.zoom(_accumulatedScrollDelta, velocityDelegate?.velocity); + await cameraDelegate?.zoom( + _accumulatedScrollDelta, velocityDelegate?.velocity); _accumulatedScrollDelta = 0.0; } } - @override + @override Future onPointerDown(Offset localPosition, int buttons) async { velocityDelegate?.stopDeceleration(); _activePointers++; @@ -120,13 +129,18 @@ class DelegateGestureHandler implements ThermionGestureHandler { } @override - Future onPointerMove(Offset localPosition, Offset delta, int buttons) async { + Future onPointerMove( + Offset localPosition, Offset delta, int buttons) async { GestureType gestureType = _getGestureTypeFromButtons(buttons); if (gestureType == GestureType.MMB_HOLD_AND_MOVE || - (_actions[GestureType.POINTER_MOVE] == GestureAction.ROTATE_CAMERA && gestureType == GestureType.POINTER_MOVE)) { - _accumulatedDeltas[GestureType.MMB_HOLD_AND_MOVE] = (_accumulatedDeltas[GestureType.MMB_HOLD_AND_MOVE] ?? Offset.zero) + delta; + (_actions[GestureType.POINTER_MOVE] == GestureAction.ROTATE_CAMERA && + gestureType == GestureType.POINTER_MOVE)) { + _accumulatedDeltas[GestureType.MMB_HOLD_AND_MOVE] = + (_accumulatedDeltas[GestureType.MMB_HOLD_AND_MOVE] ?? Offset.zero) + + delta; } 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) { - if (buttons & kPrimaryMouseButton != 0) return GestureType.LMB_HOLD_AND_MOVE; - if (buttons & kMiddleMouseButton != 0 || _isMiddleMouseButtonPressed) return GestureType.MMB_HOLD_AND_MOVE; + if (buttons & kPrimaryMouseButton != 0) + return GestureType.LMB_HOLD_AND_MOVE; + if (buttons & kMiddleMouseButton != 0 || _isMiddleMouseButtonPressed) + return GestureType.MMB_HOLD_AND_MOVE; return GestureType.POINTER_MOVE; } @override Future onPointerHover(Offset localPosition, Offset delta) async { 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 Future onPointerScroll(Offset localPosition, double scrollDelta) async { if (_actions[GestureType.SCROLLWHEEL] != GestureAction.ZOOM_CAMERA) { - throw Exception("Unsupported action: ${_actions[GestureType.SCROLLWHEEL]}"); + throw Exception( + "Unsupported action: ${_actions[GestureType.SCROLLWHEEL]}"); } try { @@ -203,6 +221,9 @@ class DelegateGestureHandler implements ThermionGestureHandler { } bool _handleKeyEvent(KeyEvent event) { + if (_actions[GestureType.KEYDOWN] == GestureAction.NONE) { + return false; + } if (event is KeyDownEvent || event is KeyRepeatEvent) { cameraDelegate?.onKeypress(event.physicalKey); return true; @@ -212,4 +233,4 @@ class DelegateGestureHandler implements ThermionGestureHandler { } return false; } -} \ No newline at end of file +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegates.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegates.dart index f85a7935..e7bf15ec 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegates.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegates.dart @@ -5,7 +5,7 @@ import 'package:vector_math/vector_math_64.dart'; abstract class CameraDelegate { Future rotate(Offset delta, Vector2? velocity); Future pan(Offset delta, Vector2? velocity); - Future zoom(double scrollDelta, Vector2? velocity); + Future zoom(double yScrollDeltaInPixels, Vector2? velocity); Future onKeypress(PhysicalKeyboardKey key); Future onKeyRelease(PhysicalKeyboardKey key); } diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/fixed_orbit_camera_rotation_delegate.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/fixed_orbit_camera_rotation_delegate.dart index 24fb612e..57675aec 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/fixed_orbit_camera_rotation_delegate.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/fixed_orbit_camera_rotation_delegate.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'dart:ui'; 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 { 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; Offset _accumulatedRotationDelta = Offset.zero; double _accumulatedZoomDelta = 0.0; - + + static final _up = Vector3(0, 1, 0); + Timer? _updateTimer; - - FixedOrbitRotateCameraDelegate(this.viewer) { - _zoomCameraDelegate = DefaultZoomCameraDelegate(this.viewer); + + Vector3 _targetPosition = Vector3(0, 0, 0); + + 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(); } @@ -49,64 +59,88 @@ class FixedOrbitRotateCameraDelegate implements CameraDelegate { } @override - Future zoom(double scrollDelta, Vector2? velocity) async { - _accumulatedZoomDelta += scrollDelta; + Future zoom(double yScrollDeltaInPixels, Vector2? velocity) async { + if (yScrollDeltaInPixels > 1) { + _accumulatedZoomDelta++; + } else { + _accumulatedZoomDelta--; + } } Future _applyAccumulatedUpdates() async { - if (_accumulatedRotationDelta != Offset.zero || _accumulatedZoomDelta != 0.0) { - Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); - Vector3 currentPosition = currentModelMatrix.getTranslation(); - 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); + if (_accumulatedRotationDelta.distanceSquared == 0.0 && + _accumulatedZoomDelta == 0.0) { + return; } + + 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 Future onKeyRelease(PhysicalKeyboardKey key) async { - //ignore + // Ignore } @override - Future onKeypress(PhysicalKeyboardKey key) async { - //ignore + Future onKeypress(PhysicalKeyboardKey key) async { + // Ignore } -} \ No newline at end of file +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/free_flight_camera_delegate.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/free_flight_camera_delegate.dart index 73bde712..2a97290f 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/free_flight_camera_delegate.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/free_flight_camera_delegate.dart @@ -9,8 +9,6 @@ import 'package:vector_math/vector_math_64.dart'; class FreeFlightCameraDelegate implements CameraDelegate { final ThermionViewer viewer; - final bool lockPitch; - final bool lockYaw; final bool lockRoll; final Vector3? minBounds; final Vector3? maxBounds; @@ -36,8 +34,7 @@ class FreeFlightCameraDelegate implements CameraDelegate { FreeFlightCameraDelegate( this.viewer, { - this.lockPitch = false, - this.lockYaw = false, + this.lockRoll = false, this.minBounds, this.maxBounds, @@ -76,9 +73,9 @@ class FreeFlightCameraDelegate implements CameraDelegate { // Apply rotation if (_accumulatedRotation != Offset.zero) { - double deltaX = lockYaw ? 0 : _accumulatedRotation.dx * rotationSensitivity * viewer.pixelRatio; - double deltaY = lockPitch ? 0 : _accumulatedRotation.dy * rotationSensitivity * viewer.pixelRatio; - double deltaZ = lockRoll ? 0 : (_accumulatedRotation.dx + _accumulatedRotation.dy) * rotationSensitivity * 0.5 * viewer.pixelRatio; + double deltaX = _accumulatedRotation.dx * rotationSensitivity * viewer.pixelRatio; + double deltaY = _accumulatedRotation.dy * rotationSensitivity * viewer.pixelRatio; + double deltaZ = (_accumulatedRotation.dx + _accumulatedRotation.dy) * rotationSensitivity * 0.5 * viewer.pixelRatio; Quaternion yawRotation = Quaternion.axisAngle(_up, -deltaX); Quaternion pitchRotation = Quaternion.axisAngle(_right, -deltaY); @@ -150,7 +147,7 @@ class FreeFlightCameraDelegate implements CameraDelegate { @override Future zoom(double scrollDelta, Vector2? velocity) async { - _accumulatedZoom += scrollDelta; + _accumulatedZoom -= scrollDelta; _lastVelocity = velocity; }