From 4b742fea2d5693f6c232c85fba36c7c1221cb3cb Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Wed, 11 Sep 2024 18:05:18 +0800 Subject: [PATCH] (flutter) add experimental GestureHandler widget and decouple from ThermionGestureDetectorDesktop --- .../gestures/thermion_gesture_handler.dart | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart 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 new file mode 100644 index 00000000..a5f9ec4a --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart @@ -0,0 +1,193 @@ +import 'dart:async'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; +import 'package:thermion_dart/thermion_dart/entities/abstract_gizmo.dart'; +import 'package:thermion_dart/thermion_dart/thermion_viewer.dart'; + +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); + } +}