diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/configurable_pan_rotate_gesture_handler.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/configurable_pan_rotate_gesture_handler.dart deleted file mode 100644 index c0aa1afa..00000000 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/configurable_pan_rotate_gesture_handler.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'dart:async'; -import 'package:flutter/gestures.dart'; -import 'package:logging/logging.dart'; -import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; -import 'package:thermion_flutter/thermion/widgets/camera/gestures/thermion_gesture_handler.dart'; - -class ConfigurablePanRotateGestureHandler implements ThermionGestureHandler { - - final ThermionViewer viewer; - final Logger _logger = Logger("ConfigurablePanRotateGestureHandler"); - - ConfigurablePanRotateGestureHandler({ - required this.viewer, - }); - - @override - Future onPointerHover(Offset localPosition) async { - // noop - } - - // @override - // Future onPointerScroll(Offset localPosition, double scrollDelta) async { - // await _zoom(localPosition, scrollDelta); - // } - - // @override - // Future onPointerDown(Offset localPosition, int buttons) async { - // if (buttons == kMiddleMouseButton) { - // await viewer.rotateStart(localPosition.dx, localPosition.dy); - // } else if (buttons == kPrimaryMouseButton) { - // await viewer.panStart(localPosition.dx, localPosition.dy); - // } - // } - - // @override - // Future onPointerMove( - // Offset localPosition, Offset delta, int buttons) async { - // switch (_currentState) { - // case ThermionGestureState.NULL: - // break; - // case ThermionGestureState.ENTITY_HIGHLIGHTED: - // await _handleEntityHighlightedMove(localPosition); - // break; - // case ThermionGestureState.GIZMO_ATTACHED: - // break; - // case ThermionGestureState.ROTATING: - // if (enableCamera) { - // await viewer.rotateUpdate(localPosition.dx, localPosition.dy); - // } - // break; - // case ThermionGestureState.PANNING: - // if (enableCamera) { - // await viewer.panUpdate(localPosition.dx, localPosition.dy); - // } - // break; - // } - // } - - // @override - // Future onPointerUp(int buttons) async { - // switch (_currentState) { - // case ThermionGestureState.ROTATING: - // await viewer.rotateEnd(); - // _currentState = ThermionGestureState.NULL; - // break; - // case ThermionGestureState.PANNING: - // await viewer.panEnd(); - // _currentState = ThermionGestureState.NULL; - // break; - // default: - // break; - // } - // } - - // Future _handleEntityHighlightedMove(Offset localPosition) async { - // if (_highlightedEntity != null) { - // await viewer.queuePositionUpdateFromViewportCoords( - // _highlightedEntity!, - // localPosition.dx, - // localPosition.dy, - // ); - // } - // } - - // Future _zoom(Offset localPosition, double scrollDelta) async { - // _scrollTimer?.cancel(); - // await viewer.zoomBegin(); - // await viewer.zoomUpdate( - // localPosition.dx, localPosition.dy, scrollDelta > 0 ? 1 : -1); - - // _scrollTimer = Timer(const Duration(milliseconds: 100), () async { - // await viewer.zoomEnd(); - // }); - // } - - @override - void dispose() { - } - - @override - Future get initialized => viewer.initialized; - - @override - GestureAction getActionForType(GestureType type) { - // TODO: implement getActionForType - throw UnimplementedError(); - } - - @override - Future onScaleEnd() { - // TODO: implement onScaleEnd - throw UnimplementedError(); - } - - @override - Future onScaleStart() { - // TODO: implement onScaleStart - throw UnimplementedError(); - } - - @override - Future onScaleUpdate() { - // TODO: implement onScaleUpdate - throw UnimplementedError(); - } - - @override - void setActionForType(GestureType type, GestureAction action) { - // TODO: implement setActionForType - } - - @override - Future onPointerDown(Offset localPosition, int buttons) { - // TODO: implement onPointerDown - throw UnimplementedError(); - } - - @override - Future onPointerMove(Offset localPosition, Offset delta, int buttons) { - // TODO: implement onPointerMove - throw UnimplementedError(); - } - - @override - Future onPointerScroll(Offset localPosition, double scrollDelta) { - // TODO: implement onPointerScroll - throw UnimplementedError(); - } - - @override - Future onPointerUp(int buttons) { - // TODO: implement onPointerUp - throw UnimplementedError(); - } -} 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 bd7488f1..8800348b 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 @@ -10,7 +10,6 @@ import 'package:thermion_flutter/thermion/widgets/camera/gestures/thermion_gestu // Renamed implementation class PickingCameraGestureHandler implements ThermionGestureHandler { - final ThermionViewer viewer; final bool enableCamera; final bool enablePicking; @@ -71,6 +70,7 @@ class PickingCameraGestureHandler implements ThermionGestureHandler { _highlightedEntity = targetEntity; if (_highlightedEntity != null) { await viewer.setStencilHighlight(_highlightedEntity!); + _gizmo?.attach(_highlightedEntity!); } } } @@ -92,10 +92,11 @@ class PickingCameraGestureHandler implements ThermionGestureHandler { @override Future onPointerScroll(Offset localPosition, double scrollDelta) async { - if(!enableCamera) { + if (!enableCamera) { return; } - if (_currentState == ThermionGestureState.NULL || _currentState == ThermionGestureState.ZOOMING) { + if (_currentState == ThermionGestureState.NULL || + _currentState == ThermionGestureState.ZOOMING) { await _zoom(localPosition, scrollDelta); } } @@ -127,7 +128,7 @@ class PickingCameraGestureHandler implements ThermionGestureHandler { await _handleEntityHighlightedMove(localPosition); return; } - + switch (_currentState) { case ThermionGestureState.NULL: break; 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 2be95fe3..29a56121 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 @@ -2,8 +2,21 @@ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; +import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; -enum GestureType { POINTER_HOVER, POINTER_DOWN, SCALE } +enum GestureType { + POINTER1_DOWN, + POINTER1_MOVE, + POINTER1_UP, + POINTER1_HOVER, + POINTER2_DOWN, + POINTER2_MOVE, + POINTER2_UP, + POINTER2_HOVER, + SCALE1, + SCALE2, + POINTER_ZOOM, +} enum GestureAction { PAN_CAMERA, @@ -21,8 +34,6 @@ enum ThermionGestureState { } abstract class ThermionGestureHandler { - GestureAction getActionForType(GestureType type); - void setActionForType(GestureType type, GestureAction action); Future onPointerHover(Offset localPosition); Future onPointerScroll(Offset localPosition, double scrollDelta); Future onPointerDown(Offset localPosition, int buttons); @@ -33,190 +44,7 @@ abstract class ThermionGestureHandler { Future onScaleEnd(); Future get initialized; void dispose(); + + void setActionForType(GestureType gestureType, GestureAction gestureAction); + GestureAction? getActionForType(GestureType gestureType); } - -// enum ThermionGestureState { -// NULL, -// ENTITY_HIGHLIGHTED, -// GIZMO_ATTACHED, -// ROTATING, -// PANNING, -// } - -// class ThermionGestureHandler { -// final ThermionViewer viewer; -// final bool enableCamera; -// final bool enablePicking; -// final Logger _logger = Logger("ThermionGestureHandler"); - -// ThermionGestureState _currentState = ThermionGestureState.NULL; -// AbstractGizmo? _gizmo; -// Timer? _scrollTimer; -// ThermionEntity? _highlightedEntity; -// StreamSubscription? _pickResultSubscription; - -// ThermionGestureHandler({ -// required this.viewer, -// this.enableCamera = true, -// this.enablePicking = true, -// }) { -// try { -// _gizmo = viewer.gizmo; -// } catch (err) { -// _logger.warning( -// "Failed to get gizmo. If you are running on WASM, this is expected"); -// } - -// _pickResultSubscription = viewer.pickResult.listen(_onPickResult); - -// // Add keyboard listener -// RawKeyboard.instance.addListener(_handleKeyEvent); -// } - -// void _handleKeyEvent(RawKeyEvent event) { -// if (event is RawKeyDownEvent && -// event.logicalKey == LogicalKeyboardKey.escape) { -// _resetToNullState(); -// } -// } - -// void _resetToNullState() async { -// // set current state to NULL first, so that any subsequent pointer movements -// // won't attempt to translate a deleted entity -// _currentState = ThermionGestureState.NULL; -// if (_highlightedEntity != null) { -// await viewer.removeStencilHighlight(_highlightedEntity!); -// _highlightedEntity = null; -// } -// } - -// void _onPickResult(FilamentPickResult result) async { -// var targetEntity = await viewer.getAncestor(result.entity) ?? result.entity; - -// if (_highlightedEntity != targetEntity) { -// if (_highlightedEntity != null) { -// await viewer.removeStencilHighlight(_highlightedEntity!); -// } - -// _highlightedEntity = targetEntity; -// if (_highlightedEntity != null) { -// await viewer.setStencilHighlight(_highlightedEntity!); -// } - -// _currentState = _highlightedEntity != null -// ? ThermionGestureState.ENTITY_HIGHLIGHTED -// : ThermionGestureState.NULL; -// } -// } - -// Future onPointerHover(Offset localPosition) async { -// if (_currentState == ThermionGestureState.GIZMO_ATTACHED) { -// _gizmo?.checkHover(localPosition.dx, localPosition.dy); -// } - -// // Update highlighted entity position -// if (_highlightedEntity != null) { -// await viewer.queuePositionUpdateFromViewportCoords( -// _highlightedEntity!, -// localPosition.dx, -// localPosition.dy, -// ); -// } -// } - -// Future onPointerScroll(Offset localPosition, double scrollDelta) async { -// if (_currentState == ThermionGestureState.NULL && enableCamera) { -// await _zoom(localPosition, scrollDelta); -// } -// } - -// Future onPointerDown(Offset localPosition, int buttons) async { -// if (_currentState == ThermionGestureState.ENTITY_HIGHLIGHTED) { -// _resetToNullState(); -// return; -// } - -// if (enablePicking && buttons != kMiddleMouseButton) { -// viewer.pick(localPosition.dx.toInt(), localPosition.dy.toInt()); -// } - -// if (buttons == kMiddleMouseButton && enableCamera) { -// await viewer.rotateStart(localPosition.dx, localPosition.dy); -// _currentState = ThermionGestureState.ROTATING; -// } else if (buttons == kPrimaryMouseButton && enableCamera) { -// await viewer.panStart(localPosition.dx, localPosition.dy); -// _currentState = ThermionGestureState.PANNING; -// } -// } - -// Future onPointerMove( -// Offset localPosition, Offset delta, int buttons) async { -// switch (_currentState) { -// case ThermionGestureState.NULL: -// // This case should not occur now, as we set the state on pointer down -// break; -// case ThermionGestureState.ENTITY_HIGHLIGHTED: -// await _handleEntityHighlightedMove(localPosition); -// break; -// case ThermionGestureState.GIZMO_ATTACHED: -// // Do nothing -// break; -// case ThermionGestureState.ROTATING: -// if (enableCamera) { -// await viewer.rotateUpdate(localPosition.dx, localPosition.dy); -// } -// break; -// case ThermionGestureState.PANNING: -// if (enableCamera) { -// await viewer.panUpdate(localPosition.dx, localPosition.dy); -// } -// break; -// } -// } - -// Future onPointerUp(int buttons) async { -// switch (_currentState) { -// case ThermionGestureState.ROTATING: -// await viewer.rotateEnd(); -// _currentState = ThermionGestureState.NULL; -// break; -// case ThermionGestureState.PANNING: -// await viewer.panEnd(); -// _currentState = ThermionGestureState.NULL; -// break; -// default: -// // For other states, no action needed -// break; -// } -// } - -// Future _handleEntityHighlightedMove(Offset localPosition) async { -// if (_highlightedEntity != null) { -// await viewer.queuePositionUpdateFromViewportCoords( -// _highlightedEntity!, -// localPosition.dx, -// localPosition.dy, -// ); -// } -// } - -// Future _zoom(Offset localPosition, double scrollDelta) async { -// _scrollTimer?.cancel(); -// await viewer.zoomBegin(); -// await viewer.zoomUpdate( -// localPosition.dx, localPosition.dy, scrollDelta > 0 ? 1 : -1); - -// _scrollTimer = Timer(const Duration(milliseconds: 100), () async { -// await viewer.zoomEnd(); -// }); -// } - -// void dispose() { -// _pickResultSubscription?.cancel(); -// if (_highlightedEntity != null) { -// viewer.removeStencilHighlight(_highlightedEntity!); -// } -// // Remove keyboard listener -// RawKeyboard.instance.removeListener(_handleKeyEvent); -// } -// } 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 3295c58a..0a9b82a7 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 @@ -1,14 +1,13 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:thermion_flutter/thermion/widgets/camera/gestures/thermion_gesture_handler.dart'; -import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_desktop_widget.dart'; -import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_mobile_widget.dart'; /// /// A widget that captures swipe/pointer events. -/// This is a dumb listener that simply forwards events to the provided [ThermionGestureHandler]. +/// This is a dumb listener; events are forwarded to a [ThermionGestureHandler]. /// class ThermionListenerWidget extends StatelessWidget { /// @@ -23,16 +22,41 @@ class ThermionListenerWidget extends StatelessWidget { /// final ThermionGestureHandler gestureHandler; - ThermionListenerWidget({ + const ThermionListenerWidget({ Key? key, required this.gestureHandler, this.child, }) : super(key: key); - bool get isDesktop => kIsWeb || - Platform.isLinux || - Platform.isWindows || - Platform.isMacOS; + bool get isDesktop => + kIsWeb || Platform.isLinux || Platform.isWindows || Platform.isMacOS; + + Widget _desktop() { + return Listener( + onPointerHover: (event) => + gestureHandler.onPointerHover(event.localPosition), + onPointerSignal: (PointerSignalEvent pointerSignal) { + if (pointerSignal is PointerScrollEvent) { + gestureHandler.onPointerScroll( + pointerSignal.localPosition, pointerSignal.scrollDelta.dy); + } + }, + onPointerPanZoomStart: (pzs) { + throw Exception("TODO - is this a pinch zoom on laptop trackpad?"); + }, + onPointerDown: (d) => + gestureHandler.onPointerDown(d.localPosition, d.buttons), + onPointerMove: (d) => + gestureHandler.onPointerMove(d.localPosition, d.delta, d.buttons), + onPointerUp: (d) => gestureHandler.onPointerUp(d.buttons), + child: child, + ); + } + + Widget _mobile() { + return _MobileListenerWidget( + gestureHandler: gestureHandler); + } @override Widget build(BuildContext context) { @@ -43,15 +67,66 @@ class ThermionListenerWidget extends StatelessWidget { return child ?? Container(); } return Stack(children: [ - if(child != null) - Positioned.fill(child:child!), + if (child != null) Positioned.fill(child: child!), if (isDesktop) - Positioned.fill(child:ThermionGestureDetectorDesktop( - gestureHandler: gestureHandler)), - if(!isDesktop) - Positioned.fill(child:ThermionGestureDetectorMobile( - gestureHandler: gestureHandler)) + Positioned.fill( + child: _desktop()), + if (!isDesktop) + Positioned.fill( + child: _mobile()) ]); }); } } + + +class _MobileListenerWidget extends StatefulWidget { + final ThermionGestureHandler gestureHandler; + + const _MobileListenerWidget( + {Key? key, required this.gestureHandler}) + : super(key: key); + + @override + State createState() => _MobileListenerWidgetState(); +} + +class _MobileListenerWidgetState + extends State<_MobileListenerWidget> { + GestureAction current = GestureAction.PAN_CAMERA; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTapDown: (details) => + widget.gestureHandler.onPointerDown(details.localPosition, 0), + onDoubleTap: () { + if (current == GestureAction.PAN_CAMERA) { + widget.gestureHandler.setActionForType( + GestureType.SCALE1, GestureAction.ROTATE_CAMERA); + current = GestureAction.ROTATE_CAMERA; + } else { + widget.gestureHandler.setActionForType( + GestureType.SCALE1, GestureAction.PAN_CAMERA); + current = GestureAction.PAN_CAMERA; + } + }, + onScaleStart: (details) async { + await widget.gestureHandler.onScaleStart(); + }, + onScaleUpdate: (details) async { + await widget.gestureHandler.onScaleUpdate(); + }, + onScaleEnd: (details) async { + await widget.gestureHandler.onScaleUpdate(); + }, + ); + + } +} 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 new file mode 100644 index 00000000..5c58f14a --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_pan_camera_delegate.dart @@ -0,0 +1,18 @@ +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'; + +class DefaultPanCameraDelegate implements PanCameraDelegate { + final ThermionViewer viewer; + + DefaultPanCameraDelegate(this.viewer); + + @override + Future panCamera(Offset delta, Vector2? velocity) async { + // Implement panning logic here + // This is a placeholder implementation + print("Panning camera by $delta"); + } +} \ No newline at end of file diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_velocity_delegate.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_velocity_delegate.dart new file mode 100644 index 00000000..a25eb0c6 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_velocity_delegate.dart @@ -0,0 +1,46 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.dart'; +import 'package:vector_math/vector_math_64.dart'; + +class DefaultVelocityDelegate extends VelocityDelegate { + Vector2? _velocity; + Timer? _decelerationTimer; + final double _decelerationFactor = 0.95; + final double _minVelocity = 0.01; + + Vector2? get velocity => _velocity; + + @override + void updateVelocity(Offset delta) { + _velocity = Vector2(delta.dx, delta.dy); + } + + @override + void startDeceleration() { + if (_velocity != null && _velocity!.length > _minVelocity) { + _decelerationTimer = Timer.periodic(Duration(milliseconds: 16), (timer) { + if (_velocity == null || _velocity!.length <= _minVelocity) { + stopDeceleration(); + return; + } + + _velocity = _velocity! * _decelerationFactor; + + }); + } + } + + @override + void stopDeceleration() { + _decelerationTimer?.cancel(); + _decelerationTimer = null; + _velocity = null; + } + + @override + void dispose() { + stopDeceleration(); + } +} 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 new file mode 100644 index 00000000..6fde49e5 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/default_zoom_camera_delegate.dart @@ -0,0 +1,38 @@ +import 'dart:math'; + +import 'package:flutter/widgets.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 DefaultZoomCameraDelegate implements ZoomCameraDelegate { + final ThermionViewer viewer; + final double _zoomSensitivity = 0.0005; + + final double? Function(Vector3 cameraPosition)? getDistanceToTarget; + + DefaultZoomCameraDelegate(this.viewer, {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(); + + double? distanceToTarget = getDistanceToTarget?.call(cameraPosition); + double zoomDistance = scrollDelta * _zoomSensitivity; + if (distanceToTarget != null) { + zoomDistance *= distanceToTarget; + if (zoomDistance.abs() < 0.0001) { + zoomDistance = scrollDelta * _zoomSensitivity; + } + } + zoomDistance = max(zoomDistance, scrollDelta * _zoomSensitivity); + + 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 new file mode 100644 index 00000000..84fd329f --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegate_gesture_handler.dart @@ -0,0 +1,127 @@ +import 'dart:async'; +import 'package:flutter/gestures.dart'; +import 'package:logging/logging.dart'; +import 'package:thermion_flutter/thermion/widgets/camera/gestures/v2/delegates.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; + VelocityDelegate? velocityDelegate; + + DelegateGestureHandler({ + required this.viewer, + required this.rotateCameraDelegate, + required this.panCameraDelegate, + required this.zoomCameraDelegate, + required this.velocityDelegate, + }); + + @override + Future onPointerDown(Offset localPosition, int buttons) async { + velocityDelegate?.stopDeceleration(); + } + + @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 == kSecondaryMouseButton) { + gestureType = GestureType.POINTER2_MOVE; + } else { + throw Exception("Unsupported button: $buttons"); + } + + 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 "); + } + } + + @override + Future onPointerUp(int buttons) async { + _currentState = ThermionGestureState.NULL; + velocityDelegate?.startDeceleration(); + } + + @override + Future onPointerHover(Offset localPosition) async { + // TODO, currently noop + } + + @override + Future onPointerScroll(Offset localPosition, double scrollDelta) async { + if (_currentState != ThermionGestureState.NULL) { + return; + } + + 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); + } catch (e) { + _logger.warning("Error during camera zoom: $e"); + } finally { + _currentState = ThermionGestureState.NULL; + } + } + + @override + void dispose() { + // Clean up any resources if needed + } + + @override + Future get initialized => viewer.initialized; + + @override + Future onScaleEnd() async {} + + @override + Future onScaleStart() async {} + + @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; + } + + GestureAction? getActionForType(GestureType gestureType) { + return _actions[gestureType]; + } +} 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 new file mode 100644 index 00000000..0f07aa24 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/delegates.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +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 VelocityDelegate { + Vector2? get velocity; + + void updateVelocity(Offset delta); + + void startDeceleration(); + + void stopDeceleration(); + + void dispose() { + stopDeceleration(); + } +} 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 new file mode 100644 index 00000000..15998b9d --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/fixed_orbit_camera_rotation_delegate.dart @@ -0,0 +1,45 @@ +import 'dart:ui'; + +import 'package:flutter/widgets.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 FixedOrbitRotateCameraDelegate implements RotateCameraDelegate { + final ThermionViewer viewer; + static final _up = Vector3(0, 1, 0); + static final _forward = Vector3(0, 0, -1); + + static const double _rotationSensitivity = 0.01; + + FixedOrbitRotateCameraDelegate(this.viewer); + + @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); + } +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_mobile_widget.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_mobile_widget.dart index b6907d96..2031f72b 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_mobile_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_mobile_widget.dart @@ -5,11 +5,9 @@ class ThermionGestureDetectorMobile extends StatefulWidget { final Widget? child; final ThermionGestureHandler gestureHandler; - const ThermionGestureDetectorMobile({ - Key? key, - required this.gestureHandler, - this.child, - }) : super(key: key); + const ThermionGestureDetectorMobile( + {Key? key, required this.gestureHandler, this.child}) + : super(key: key); @override State createState() => _ThermionGestureDetectorMobileState(); @@ -17,6 +15,13 @@ class ThermionGestureDetectorMobile extends StatefulWidget { class _ThermionGestureDetectorMobileState extends State { + GestureAction current = GestureAction.PAN_CAMERA; + + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { return Stack(children: [ @@ -26,18 +31,21 @@ class _ThermionGestureDetectorMobileState onTapDown: (details) => widget.gestureHandler.onPointerDown(details.localPosition, 0), onDoubleTap: () { - var current = widget.gestureHandler.getActionForType(GestureType.SCALE); - if(current == GestureAction.PAN_CAMERA) { - widget.gestureHandler.setActionForType(GestureType.SCALE, GestureAction.ROTATE_CAMERA); + if (current == GestureAction.PAN_CAMERA) { + widget.gestureHandler.setActionForType( + GestureType.SCALE1, GestureAction.ROTATE_CAMERA); + current = GestureAction.ROTATE_CAMERA; } else { - widget.gestureHandler.setActionForType(GestureType.SCALE, GestureAction.PAN_CAMERA); + widget.gestureHandler.setActionForType( + GestureType.SCALE1, GestureAction.PAN_CAMERA); + current = GestureAction.PAN_CAMERA; } }, onScaleStart: (details) async { await widget.gestureHandler.onScaleStart(); }, onScaleUpdate: (details) async { - await widget.gestureHandler.onScaleUpdate(); + await widget.gestureHandler.onScaleUpdate(); }, onScaleEnd: (details) async { await widget.gestureHandler.onScaleUpdate(); @@ -45,7 +53,6 @@ class _ThermionGestureDetectorMobileState child: widget.child, ), ), - ]); } }