diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/picking_camera_gesture_handler.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/picking_camera_gesture_handler.dart index 8800348b..09f723cf 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/picking_camera_gesture_handler.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/picking_camera_gesture_handler.dart @@ -76,7 +76,7 @@ class PickingCameraGestureHandler implements ThermionGestureHandler { } @override - Future onPointerHover(Offset localPosition) async { + Future onPointerHover(Offset localPosition, Offset delta) async { if (_gizmoAttached) { _gizmo?.checkHover(localPosition.dx, localPosition.dy); } diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart index 29a56121..0306afa4 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart @@ -5,17 +5,18 @@ import 'package:flutter/services.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; enum GestureType { - POINTER1_DOWN, - POINTER1_MOVE, - POINTER1_UP, - POINTER1_HOVER, - POINTER2_DOWN, - POINTER2_MOVE, - POINTER2_UP, - POINTER2_HOVER, + LMB_DOWN, + LMB_HOLD_AND_MOVE, + LMB_UP, + LMB_HOVER, + MMB_DOWN, + MMB_HOLD_AND_MOVE, + MMB_UP, + MMB_HOVER, SCALE1, SCALE2, - POINTER_ZOOM, + SCROLLWHEEL, + POINTER_MOVE } enum GestureAction { @@ -23,7 +24,8 @@ enum GestureAction { ROTATE_CAMERA, ZOOM_CAMERA, TRANSLATE_ENTITY, - ROTATE_ENTITY + ROTATE_ENTITY, + NONE } enum ThermionGestureState { @@ -34,7 +36,7 @@ enum ThermionGestureState { } abstract class ThermionGestureHandler { - Future onPointerHover(Offset localPosition); + Future onPointerHover(Offset localPosition, Offset delta); Future onPointerScroll(Offset localPosition, double scrollDelta); Future onPointerDown(Offset localPosition, int buttons); Future onPointerMove(Offset localPosition, Offset delta, int buttons); diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_listener_widget.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_listener_widget.dart index 0a9b82a7..0dcb1642 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_listener_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_listener_widget.dart @@ -34,7 +34,7 @@ class ThermionListenerWidget extends StatelessWidget { Widget _desktop() { return Listener( onPointerHover: (event) => - gestureHandler.onPointerHover(event.localPosition), + gestureHandler.onPointerHover(event.localPosition, event.delta), onPointerSignal: (PointerSignalEvent pointerSignal) { if (pointerSignal is PointerScrollEvent) { gestureHandler.onPointerScroll( diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_keyboard_camera_flight_delegate.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_keyboard_camera_flight_delegate.dart new file mode 100644 index 00000000..4630acbc --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_keyboard_camera_flight_delegate.dart @@ -0,0 +1,87 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; +import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; +import 'package:vector_math/vector_math_64.dart'; + +class DefaultKeyboardCameraFlightDelegate + { + final ThermionViewer viewer; + + static const double _panSensitivity = 0.005; + static const double _keyMoveSensitivity = 0.1; + + final Map _pressedKeys = {}; + Timer? _moveTimer; + + DefaultKeyboardCameraFlightDelegate(this.viewer) { + _startMoveLoop(); + } + + @override + Future panCamera(Offset delta, Vector2? velocity) async { + double deltaX = delta.dx; + double deltaY = delta.dy; + deltaX *= _panSensitivity * viewer.pixelRatio; + deltaY *= _panSensitivity * viewer.pixelRatio; + + await _moveCamera(deltaX, deltaY, 0); + } + + @override + Future onKeypress(PhysicalKeyboardKey key) async { + _pressedKeys[key] = true; + } + + // New method to handle key release + Future onKeyRelease(PhysicalKeyboardKey key) async { + _pressedKeys.remove(key); + } + + void _startMoveLoop() { + _moveTimer = Timer.periodic( + Duration(milliseconds: 16), (_) => _processKeyboardInput()); + } + + Future _processKeyboardInput() async { + double dx = 0, dy = 0, dz = 0; + + if (_pressedKeys[PhysicalKeyboardKey.keyW] == true) + dz += _keyMoveSensitivity; + if (_pressedKeys[PhysicalKeyboardKey.keyS] == true) + dz -= _keyMoveSensitivity; + if (_pressedKeys[PhysicalKeyboardKey.keyA] == true) + dx -= _keyMoveSensitivity; + if (_pressedKeys[PhysicalKeyboardKey.keyD] == true) + dx += _keyMoveSensitivity; + + if (dx != 0 || dy != 0 || dz != 0) { + await _moveCamera(dx, dy, dz); + } + // Removed _pressedKeys.clear(); from here + } + + Future _moveCamera(double dx, double dy, double dz) async { + Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); + Vector3 currentPosition = currentModelMatrix.getTranslation(); + Quaternion currentRotation = + Quaternion.fromRotation(currentModelMatrix.getRotation()); + + Vector3 forward = Vector3(0, 0, -1)..applyQuaternion(currentRotation); + Vector3 right = Vector3(1, 0, 0)..applyQuaternion(currentRotation); + Vector3 up = Vector3(0, 1, 0)..applyQuaternion(currentRotation); + + Vector3 moveOffset = right * dx + up * dy + forward * dz; + Vector3 newPosition = currentPosition + moveOffset; + + Matrix4 newModelMatrix = + Matrix4.compose(newPosition, currentRotation, Vector3(1, 1, 1)); + await viewer.setCameraModelMatrix4(newModelMatrix); + } + + void dispose() { + _moveTimer?.cancel(); + } +} \ No newline at end of file diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_pan_camera_delegate.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_pan_camera_delegate.dart index a2c9eef0..a9d05751 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_pan_camera_delegate.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_pan_camera_delegate.dart @@ -1,34 +1,35 @@ -import 'dart:ui'; +// import 'dart:ui'; -import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; -import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; -import 'package:vector_math/vector_math_64.dart'; +// import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; +// import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; +// import 'package:vector_math/vector_math_64.dart'; -class DefaultPanCameraDelegate implements PanCameraDelegate { - final ThermionViewer viewer; +// class DefaultPanCameraDelegate implements PanCameraDelegate { +// final ThermionViewer viewer; - static const double _panSensitivity = 0.005; +// static const double _panSensitivity = 0.005; - DefaultPanCameraDelegate(this.viewer); +// DefaultPanCameraDelegate(this.viewer); +// static const double _panSensitivity = 0.005; - @override - Future panCamera(Offset delta, Vector2? velocity) async { - double deltaX = delta.dx; - double deltaY = delta.dy; - deltaX *= _panSensitivity * viewer.pixelRatio; - deltaY *= _panSensitivity * viewer.pixelRatio; +// @override +// Future panCamera(Offset delta, Vector2? velocity) async { +// double deltaX = delta.dx; +// double deltaY = delta.dy; +// deltaX *= _panSensitivity * viewer.pixelRatio; +// deltaY *= _panSensitivity * viewer.pixelRatio; - Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); - Vector3 currentPosition = currentModelMatrix.getTranslation(); - Quaternion currentRotation = Quaternion.fromRotation(currentModelMatrix.getRotation()); +// Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); +// Vector3 currentPosition = currentModelMatrix.getTranslation(); +// Quaternion currentRotation = Quaternion.fromRotation(currentModelMatrix.getRotation()); - Vector3 right = Vector3(1, 0, 0)..applyQuaternion(currentRotation); - Vector3 up = Vector3(0, 1, 0)..applyQuaternion(currentRotation); +// Vector3 right = Vector3(1, 0, 0)..applyQuaternion(currentRotation); +// Vector3 up = Vector3(0, 1, 0)..applyQuaternion(currentRotation); - Vector3 panOffset = right * -deltaX + up * deltaY; - Vector3 newPosition = currentPosition + panOffset; +// Vector3 panOffset = right * -deltaX + up * deltaY; +// Vector3 newPosition = currentPosition + panOffset; - Matrix4 newModelMatrix = Matrix4.compose(newPosition, currentRotation, Vector3(1, 1, 1)); - await viewer.setCameraModelMatrix4(newModelMatrix); - } -} \ No newline at end of file +// Matrix4 newModelMatrix = Matrix4.compose(newPosition, currentRotation, Vector3(1, 1, 1)); +// await viewer.setCameraModelMatrix4(newModelMatrix); +// } +// } \ No newline at end of file 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 2cbce35a..30c8cd64 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 @@ -5,23 +5,16 @@ import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; import 'package:vector_math/vector_math_64.dart'; -class DefaultZoomCameraDelegate implements ZoomCameraDelegate { +class DefaultZoomCameraDelegate { final ThermionViewer viewer; - final double zoomSensitivity ; + final double zoomSensitivity; final double? Function(Vector3 cameraPosition)? getDistanceToTarget; - DefaultZoomCameraDelegate(this.viewer, {this.zoomSensitivity = 0.005, this.getDistanceToTarget}); - - @override - Future zoomCamera(double scrollDelta, Vector2? velocity) async { - Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); - final cameraRotation = currentModelMatrix.getRotation(); - final cameraPosition = currentModelMatrix.getTranslation(); - - Vector3 forwardVector = cameraRotation.getColumn(2); - forwardVector.normalize(); + 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) { @@ -30,7 +23,20 @@ class DefaultZoomCameraDelegate implements ZoomCameraDelegate { zoomDistance = scrollDelta * zoomSensitivity; } } - zoomDistance = max(zoomDistance, scrollDelta * zoomSensitivity); + return max(zoomDistance, scrollDelta * zoomSensitivity); + } + + @override + Future zoom(double scrollDelta, Vector2? velocity) async { + Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); + final cameraRotation = currentModelMatrix.getRotation(); + final cameraPosition = currentModelMatrix.getTranslation(); + + Vector3 forwardVector = cameraRotation.getColumn(2); + forwardVector.normalize(); + + var zoomDistance = + calculateZoomDistance(scrollDelta, velocity, cameraPosition); 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 e384f737..142b73aa 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 @@ -1,161 +1,177 @@ import 'dart:async'; import 'package:flutter/gestures.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; -import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/default_pan_camera_delegate.dart'; +import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/default_keyboard_camera_flight_delegate.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/default_velocity_delegate.dart'; -import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/default_zoom_camera_delegate.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.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_flutter.dart'; class DelegateGestureHandler implements ThermionGestureHandler { final ThermionViewer viewer; final Logger _logger = Logger("CustomGestureHandler"); - ThermionGestureState _currentState = ThermionGestureState.NULL; - - // Class-based delegates - RotateCameraDelegate? rotateCameraDelegate; - PanCameraDelegate? panCameraDelegate; - ZoomCameraDelegate? zoomCameraDelegate; + CameraDelegate? cameraDelegate; VelocityDelegate? velocityDelegate; - // Timer for continuous movement - Timer? _velocityTimer; - static const _velocityUpdateInterval = Duration(milliseconds: 16); // ~60 FPS + Ticker? _ticker; + static const _updateInterval = Duration(milliseconds: 16); + + Map _accumulatedDeltas = {}; + double _accumulatedScrollDelta = 0.0; + int _activePointers = 0; + bool _isMiddleMouseButtonPressed = false; + + VoidCallback? _keyboardListenerDisposer; + + final Map _actions = { + GestureType.LMB_HOLD_AND_MOVE: GestureAction.PAN_CAMERA, + GestureType.MMB_HOLD_AND_MOVE: GestureAction.ROTATE_CAMERA, + GestureType.SCROLLWHEEL: GestureAction.ZOOM_CAMERA, + GestureType.POINTER_MOVE: GestureAction.NONE, + }; DelegateGestureHandler({ required this.viewer, - required this.rotateCameraDelegate, - required this.panCameraDelegate, - required this.zoomCameraDelegate, + required this.cameraDelegate, required this.velocityDelegate, - }); - - factory DelegateGestureHandler.withDefaults(ThermionViewer viewer) => - DelegateGestureHandler( - viewer: viewer, - rotateCameraDelegate: FixedOrbitRotateCameraDelegate(viewer), - panCameraDelegate: DefaultPanCameraDelegate(viewer), - zoomCameraDelegate: DefaultZoomCameraDelegate(viewer), - velocityDelegate: DefaultVelocityDelegate()); - - @override - Future onPointerDown(Offset localPosition, int buttons) async { - velocityDelegate?.stopDeceleration(); - _stopVelocityTimer(); + Map? actions, + }) { + _initializeKeyboardListener(); + _initializeTicker(); + if (actions != null) { + _actions.addAll(actions); + } + _initializeAccumulatedDeltas(); } - GestureType? _lastGestureType; + factory DelegateGestureHandler.fixedOrbit(ThermionViewer viewer) => + DelegateGestureHandler( + viewer: viewer, + cameraDelegate: FixedOrbitRotateCameraDelegate(viewer), + velocityDelegate: DefaultVelocityDelegate(), + ); + + factory DelegateGestureHandler.flight(ThermionViewer viewer) => + DelegateGestureHandler( + viewer: viewer, + cameraDelegate: FreeFlightCameraDelegate(viewer), + velocityDelegate: DefaultVelocityDelegate(), + actions: {GestureType.POINTER_MOVE: GestureAction.ROTATE_CAMERA}, + ); + + void _initializeAccumulatedDeltas() { + for (var gestureType in GestureType.values) { + _accumulatedDeltas[gestureType] = Offset.zero; + } + } + + void _initializeTicker() { + _ticker = Ticker(_onTick); + _ticker!.start(); + } + + void _onTick(Duration elapsed) async { + await _applyAccumulatedUpdates(); + } + + Future _applyAccumulatedUpdates() async { + for (var gestureType in GestureType.values) { + Offset delta = _accumulatedDeltas[gestureType] ?? Offset.zero; + if (delta != Offset.zero) { + velocityDelegate?.updateVelocity(delta); + + var action = _actions[gestureType]; + switch (action) { + case GestureAction.PAN_CAMERA: + await cameraDelegate?.pan(delta, velocityDelegate?.velocity); + break; + case GestureAction.ROTATE_CAMERA: + await cameraDelegate?.rotate(delta, velocityDelegate?.velocity); + break; + case GestureAction.NONE: + // Do nothing + break; + default: + _logger.warning("Unsupported gesture action: $action for type: $gestureType"); + break; + } + + _accumulatedDeltas[gestureType] = Offset.zero; + } + } + + if (_accumulatedScrollDelta != 0.0) { + await cameraDelegate?.zoom(_accumulatedScrollDelta, velocityDelegate?.velocity); + _accumulatedScrollDelta = 0.0; + } + } + + @override + Future onPointerDown(Offset localPosition, int buttons) async { + velocityDelegate?.stopDeceleration(); + _activePointers++; + if (buttons & kMiddleMouseButton != 0) { + _isMiddleMouseButtonPressed = true; + } + } @override - Future onPointerMove( - Offset localPosition, Offset delta, int buttons) async { - velocityDelegate?.updateVelocity(delta); - - GestureType gestureType; - if (buttons == kPrimaryMouseButton) { - gestureType = GestureType.POINTER1_MOVE; - } else if (buttons == kMiddleMouseButton) { - gestureType = GestureType.POINTER2_MOVE; + 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; } else { - throw Exception("Unsupported button: $buttons"); + _accumulatedDeltas[gestureType] = (_accumulatedDeltas[gestureType] ?? Offset.zero) + delta; } - - var action = _actions[gestureType]; - - switch (action) { - case GestureAction.PAN_CAMERA: - _currentState = ThermionGestureState.PANNING; - await panCameraDelegate?.panCamera(delta, velocityDelegate?.velocity); - case GestureAction.ROTATE_CAMERA: - _currentState = ThermionGestureState.ROTATING; - await rotateCameraDelegate?.rotateCamera( - delta, velocityDelegate?.velocity); - case null: - // ignore; - break; - default: - throw Exception("Unsupported gesture type : $gestureType "); - } - - _lastGestureType = gestureType; } @override Future onPointerUp(int buttons) async { - _currentState = ThermionGestureState.NULL; - velocityDelegate?.startDeceleration(); - _startVelocityTimer(); - } - - void _startVelocityTimer() { - _stopVelocityTimer(); // Ensure any existing timer is stopped - _velocityTimer = Timer.periodic(_velocityUpdateInterval, (timer) { - _applyVelocity(); - }); - } - - void _stopVelocityTimer() { - _velocityTimer?.cancel(); - _velocityTimer = null; - } - - Future _applyVelocity() async { - final velocity = velocityDelegate?.velocity; - if (velocity == null || velocity.length < 0.1) { - _stopVelocityTimer(); - return; + _activePointers--; + if (_activePointers == 0) { + velocityDelegate?.startDeceleration(); } - - final lastAction = _actions[_lastGestureType]; - switch (lastAction) { - case GestureAction.PAN_CAMERA: - await panCameraDelegate?.panCamera( - Offset(velocity.x, velocity.y), velocity); - case GestureAction.ROTATE_CAMERA: - await rotateCameraDelegate?.rotateCamera( - Offset(velocity.x, velocity.y), velocity); - default: - // Do nothing for other actions - break; + if (buttons & kMiddleMouseButton != 0) { + _isMiddleMouseButtonPressed = false; } + } - velocityDelegate?.updateVelocity(Offset(velocity.x, velocity.y)); // Gradually reduce velocity + 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; + return GestureType.POINTER_MOVE; } @override - Future onPointerHover(Offset localPosition) async { - // TODO, currently noop + 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; + } } - + @override Future onPointerScroll(Offset localPosition, double scrollDelta) async { - if (_currentState != ThermionGestureState.NULL) { - return; + if (_actions[GestureType.SCROLLWHEEL] != GestureAction.ZOOM_CAMERA) { + throw Exception("Unsupported action: ${_actions[GestureType.SCROLLWHEEL]}"); } - if (_actions[GestureType.POINTER_ZOOM] != GestureAction.ZOOM_CAMERA) { - throw Exception( - "Unsupported action : ${_actions[GestureType.POINTER_ZOOM]}"); - } - - _currentState = ThermionGestureState.ZOOMING; - try { - await zoomCameraDelegate?.zoomCamera( - scrollDelta, velocityDelegate?.velocity); + _accumulatedScrollDelta += scrollDelta; } catch (e) { - _logger.warning("Error during camera zoom: $e"); - } finally { - _currentState = ThermionGestureState.NULL; + _logger.warning("Error during scroll accumulation: $e"); } } @override void dispose() { - _stopVelocityTimer(); velocityDelegate?.dispose(); + _keyboardListenerDisposer?.call(); + _ticker?.dispose(); } @override @@ -170,12 +186,6 @@ class DelegateGestureHandler implements ThermionGestureHandler { @override Future onScaleUpdate() async {} - final _actions = { - GestureType.POINTER1_MOVE: GestureAction.PAN_CAMERA, - GestureType.POINTER2_MOVE: GestureAction.ROTATE_CAMERA, - GestureType.POINTER_ZOOM: GestureAction.ZOOM_CAMERA - }; - @override void setActionForType(GestureType gestureType, GestureAction gestureAction) { _actions[gestureType] = gestureAction; @@ -184,4 +194,22 @@ class DelegateGestureHandler implements ThermionGestureHandler { GestureAction? getActionForType(GestureType gestureType) { return _actions[gestureType]; } -} + + void _initializeKeyboardListener() { + HardwareKeyboard.instance.addHandler(_handleKeyEvent); + _keyboardListenerDisposer = () { + HardwareKeyboard.instance.removeHandler(_handleKeyEvent); + }; + } + + bool _handleKeyEvent(KeyEvent event) { + if (event is KeyDownEvent || event is KeyRepeatEvent) { + cameraDelegate?.onKeypress(event.physicalKey); + return true; + } else if (event is KeyUpEvent) { + cameraDelegate?.onKeyRelease(event.physicalKey); + return true; + } + 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 0f07aa24..f85a7935 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 @@ -1,17 +1,13 @@ import 'dart:ui'; - +import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; -abstract class RotateCameraDelegate { - Future rotateCamera(Offset delta, Vector2? velocity); -} - -abstract class PanCameraDelegate { - Future panCamera(Offset delta, Vector2? velocity); -} - -abstract class ZoomCameraDelegate { - Future zoomCamera(double scrollDelta, Vector2? velocity); +abstract class CameraDelegate { + Future rotate(Offset delta, Vector2? velocity); + Future pan(Offset delta, Vector2? velocity); + Future zoom(double scrollDelta, Vector2? velocity); + Future onKeypress(PhysicalKeyboardKey key); + Future onKeyRelease(PhysicalKeyboardKey key); } abstract class VelocityDelegate { 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 15998b9d..24fb612e 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,45 +1,112 @@ +import 'dart:async'; import 'dart:ui'; +import 'package:flutter/src/services/keyboard_key.g.dart'; import 'package:flutter/widgets.dart'; import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; +import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/default_zoom_camera_delegate.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; import 'package:vector_math/vector_math_64.dart'; -class FixedOrbitRotateCameraDelegate implements RotateCameraDelegate { +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; - FixedOrbitRotateCameraDelegate(this.viewer); + late DefaultZoomCameraDelegate _zoomCameraDelegate; + + Offset _accumulatedRotationDelta = Offset.zero; + double _accumulatedZoomDelta = 0.0; + + Timer? _updateTimer; + + FixedOrbitRotateCameraDelegate(this.viewer) { + _zoomCameraDelegate = DefaultZoomCameraDelegate(this.viewer); + _startUpdateTimer(); + } + + void _startUpdateTimer() { + _updateTimer = Timer.periodic(const Duration(milliseconds: 16), (_) { + _applyAccumulatedUpdates(); + }); + } + + void dispose() { + _updateTimer?.cancel(); + } @override - Future rotateCamera(Offset delta, Vector2? velocity) async { - double deltaX = delta.dx; - double deltaY = delta.dy; - deltaX *= _rotationSensitivity * viewer.pixelRatio; - deltaY *= _rotationSensitivity * viewer.pixelRatio; - - Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); - Vector3 currentPosition = currentModelMatrix.getTranslation(); - double distance = currentPosition.length; - Quaternion currentRotation = - Quaternion.fromRotation(currentModelMatrix.getRotation()); - - Quaternion yawRotation = Quaternion.axisAngle(_up, -deltaX); - Vector3 right = _up.cross(_forward)..normalize(); - Quaternion pitchRotation = Quaternion.axisAngle(right, -deltaY); - - Quaternion newRotation = currentRotation * yawRotation * pitchRotation; - newRotation.normalize(); - - Vector3 newPosition = _forward.clone() - ..applyQuaternion(newRotation) - ..scale(-distance); - - Matrix4 newModelMatrix = - Matrix4.compose(newPosition, newRotation, Vector3(1, 1, 1)); - await viewer.setCameraModelMatrix4(newModelMatrix); + Future rotate(Offset delta, Vector2? velocity) async { + _accumulatedRotationDelta += delta; } -} + + @override + Future pan(Offset delta, Vector2? velocity) { + throw UnimplementedError("Not supported in fixed orbit mode"); + } + + @override + Future zoom(double scrollDelta, Vector2? velocity) async { + _accumulatedZoomDelta += scrollDelta; + } + + 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); + } + } + + @override + Future onKeyRelease(PhysicalKeyboardKey key) async { + //ignore + } + + @override + 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 new file mode 100644 index 00000000..73bde712 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/free_flight_camera_delegate.dart @@ -0,0 +1,207 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; +import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; +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; + + final double rotationSensitivity; + final double movementSensitivity; + final double zoomSensitivity; + final double panSensitivity; + final double keyMoveSensitivity; + + static final _up = Vector3(0, 1, 0); + static final _forward = Vector3(0, 0, -1); + static final Vector3 _right = Vector3(1, 0, 0); + + Offset _accumulatedRotation = Offset.zero; + Offset _accumulatedPan = Offset.zero; + double _accumulatedZoom = 0.0; + Vector2? _lastVelocity; + + Ticker? _ticker; + Timer? _moveTimer; + final Map _pressedKeys = {}; + + FreeFlightCameraDelegate( + this.viewer, { + this.lockPitch = false, + this.lockYaw = false, + this.lockRoll = false, + this.minBounds, + this.maxBounds, + this.rotationSensitivity = 0.001, + this.movementSensitivity = 0.1, + this.zoomSensitivity = 0.1, + this.panSensitivity = 0.01, + this.keyMoveSensitivity = 0.1, + }) { + _initializeTicker(); + _startMoveLoop(); + } + + void _initializeTicker() { + _ticker = Ticker(_onTick); + _ticker!.start(); + } + + void _startMoveLoop() { + _moveTimer = Timer.periodic( + Duration(milliseconds: 16), (_) => _processKeyboardInput()); + } + + void _onTick(Duration elapsed) { + _applyAccumulatedUpdates(); + } + + Future _applyAccumulatedUpdates() async { + if (_accumulatedRotation != Offset.zero || + _accumulatedPan != Offset.zero || + _accumulatedZoom != 0.0) { + Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); + Vector3 currentPosition = currentModelMatrix.getTranslation(); + Quaternion currentRotation = + Quaternion.fromRotation(currentModelMatrix.getRotation()); + + // 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; + + Quaternion yawRotation = Quaternion.axisAngle(_up, -deltaX); + Quaternion pitchRotation = Quaternion.axisAngle(_right, -deltaY); + Quaternion rollRotation = Quaternion.axisAngle(_forward, deltaZ); + + currentRotation = currentRotation * yawRotation * pitchRotation * rollRotation; + currentRotation.normalize(); + + _accumulatedRotation = Offset.zero; + } + + // Apply pan + if (_accumulatedPan != Offset.zero) { + Vector3 right = _right.clone()..applyQuaternion(currentRotation); + Vector3 up = _up.clone()..applyQuaternion(currentRotation); + + double deltaX = _accumulatedPan.dx * panSensitivity * viewer.pixelRatio; + double deltaY = _accumulatedPan.dy * panSensitivity * viewer.pixelRatio; + + Vector3 newPosition = currentPosition + right * -deltaX + up * deltaY; + newPosition = _constrainPosition(newPosition); + + currentPosition = newPosition; + + _accumulatedPan = Offset.zero; + } + + // Apply zoom + if (_accumulatedZoom != 0.0) { + Vector3 forward = _forward.clone()..applyQuaternion(currentRotation); + Vector3 newPosition = currentPosition + forward * _accumulatedZoom * zoomSensitivity; + newPosition = _constrainPosition(newPosition); + + currentPosition = newPosition; + _accumulatedZoom = 0.0; + } + + Matrix4 newModelMatrix = + Matrix4.compose(currentPosition, currentRotation, Vector3(1, 1, 1)); + await viewer.setCameraModelMatrix4(newModelMatrix); + } + } + + Vector3 _constrainPosition(Vector3 position) { + if (minBounds != null) { + position.x = position.x.clamp(minBounds!.x, double.infinity); + position.y = position.y.clamp(minBounds!.y, double.infinity); + position.z = position.z.clamp(minBounds!.z, double.infinity); + } + if (maxBounds != null) { + position.x = position.x.clamp(double.negativeInfinity, maxBounds!.x); + position.y = position.y.clamp(double.negativeInfinity, maxBounds!.y); + position.z = position.z.clamp(double.negativeInfinity, maxBounds!.z); + } + return position; + } + + @override + Future rotate(Offset delta, Vector2? velocity) async { + _accumulatedRotation += delta; + _lastVelocity = velocity; + } + + @override + Future pan(Offset delta, Vector2? velocity) async { + _accumulatedPan += delta; + _lastVelocity = velocity; + } + + @override + Future zoom(double scrollDelta, Vector2? velocity) async { + _accumulatedZoom += scrollDelta; + _lastVelocity = velocity; + } + + @override + Future onKeypress(PhysicalKeyboardKey key) async { + _pressedKeys[key] = true; + } + + @override + Future onKeyRelease(PhysicalKeyboardKey key) async { + _pressedKeys.remove(key); + } + + Future _processKeyboardInput() async { + double dx = 0, dy = 0, dz = 0; + + if (_pressedKeys[PhysicalKeyboardKey.keyW] == true) + dz += keyMoveSensitivity; + if (_pressedKeys[PhysicalKeyboardKey.keyS] == true) + dz -= keyMoveSensitivity; + if (_pressedKeys[PhysicalKeyboardKey.keyA] == true) + dx -= keyMoveSensitivity; + if (_pressedKeys[PhysicalKeyboardKey.keyD] == true) + dx += keyMoveSensitivity; + + if (dx != 0 || dy != 0 || dz != 0) { + await _moveCamera(dx, dy, dz); + } + } + + Future _moveCamera(double dx, double dy, double dz) async { + Matrix4 currentModelMatrix = await viewer.getCameraModelMatrix(); + Vector3 currentPosition = currentModelMatrix.getTranslation(); + Quaternion currentRotation = + Quaternion.fromRotation(currentModelMatrix.getRotation()); + + Vector3 forward = Vector3(0, 0, -1)..applyQuaternion(currentRotation); + Vector3 right = Vector3(1, 0, 0)..applyQuaternion(currentRotation); + Vector3 up = Vector3(0, 1, 0)..applyQuaternion(currentRotation); + + Vector3 moveOffset = right * dx + up * dy + forward * dz; + Vector3 newPosition = currentPosition + moveOffset; + newPosition = _constrainPosition(newPosition); + + Matrix4 newModelMatrix = + Matrix4.compose(newPosition, currentRotation, Vector3(1, 1, 1)); + await viewer.setCameraModelMatrix4(newModelMatrix); + } + + void dispose() { + _ticker?.dispose(); + _moveTimer?.cancel(); + } +} \ No newline at end of file