(flutter) add experimental GestureHandler widget and decouple from ThermionGestureDetectorDesktop

This commit is contained in:
Nick Fisher
2024-09-11 18:05:24 +08:00
parent 0a4b5c8676
commit eefa3cbe98

View File

@@ -5,49 +5,24 @@ import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:thermion_flutter/thermion/widgets/camera/gestures/thermion_gesture_handler.dart';
import 'package:vector_math/vector_math_64.dart' as v64; import 'package:vector_math/vector_math_64.dart' as v64;
///
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
///
class ThermionGestureDetectorDesktop extends StatefulWidget { class ThermionGestureDetectorDesktop extends StatefulWidget {
///
/// The content to display below the gesture detector/listener widget.
/// This will usually be a ThermionWidget (so you can navigate by directly interacting with the viewport), but this is not necessary.
/// It is equally possible to render the viewport/gesture controls elsewhere in the widget hierarchy. The only requirement is that they share the same [FilamentController].
///
final Widget? child; final Widget? child;
///
/// The [controller] attached to the [ThermionWidget] you wish to control.
///
final ThermionViewer controller; final ThermionViewer controller;
///
/// If true, an overlay will be shown with buttons to toggle whether pointer movements are interpreted as:
/// 1) rotate or a pan (mobile only),
/// 2) moving the camera or the background image (TODO).
///
final bool showControlOverlay; final bool showControlOverlay;
///
/// If false, gestures will not manipulate the active camera.
///
final bool enableCamera; final bool enableCamera;
///
/// If false, pointer down events will not trigger hit-testing (picking).
///
final bool enablePicking; final bool enablePicking;
const ThermionGestureDetectorDesktop( const ThermionGestureDetectorDesktop({
{Key? key, Key? key,
required this.controller, required this.controller,
this.child, this.child,
this.showControlOverlay = false, this.showControlOverlay = false,
this.enableCamera = true, this.enableCamera = true,
this.enablePicking = true}) this.enablePicking = true,
: super(key: key); }) : super(key: key);
@override @override
State<StatefulWidget> createState() => _ThermionGestureDetectorDesktopState(); State<StatefulWidget> createState() => _ThermionGestureDetectorDesktopState();
@@ -55,129 +30,50 @@ class ThermionGestureDetectorDesktop extends StatefulWidget {
class _ThermionGestureDetectorDesktopState class _ThermionGestureDetectorDesktopState
extends State<ThermionGestureDetectorDesktop> { extends State<ThermionGestureDetectorDesktop> {
final _logger = Logger("_ThermionGestureDetectorDesktopState"); late ThermionGestureHandler _gestureHandler;
///
///
// ignore: unused_field
final bool _scaling = false;
bool _pointerMoving = false;
AbstractGizmo? _gizmo;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
try { _gestureHandler = ThermionGestureHandler(
_gizmo = widget.controller.gizmo; enableCamera: widget.enableCamera,
} catch (err) { enablePicking: widget.enablePicking, viewer: widget.controller,
_logger.warning( );
"Failed to get gizmo. If you are running on WASM, this is expected");
}
} }
@override @override
void didUpdateWidget(ThermionGestureDetectorDesktop oldWidget) { void didUpdateWidget(ThermionGestureDetectorDesktop oldWidget) {
if (widget.showControlOverlay != oldWidget.showControlOverlay || if (widget.enableCamera != oldWidget.enableCamera ||
widget.enableCamera != oldWidget.enableCamera ||
widget.enablePicking != oldWidget.enablePicking) { widget.enablePicking != oldWidget.enablePicking) {
setState(() {}); _gestureHandler = ThermionGestureHandler(
viewer: widget.controller,
enableCamera: widget.enableCamera,
enablePicking: widget.enablePicking,
);
} }
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
Timer? _scrollTimer;
///
/// Scroll-wheel on desktop, interpreted as zoom
///
void _zoom(PointerScrollEvent pointerSignal) async {
_scrollTimer?.cancel();
await widget.controller.zoomBegin();
await widget.controller.zoomUpdate(
pointerSignal.localPosition.dx,
pointerSignal.localPosition.dy,
pointerSignal.scrollDelta.dy > 0 ? 1 : -1);
// we don't want to end the zoom in the same frame, because this will destroy the camera manipulator (and cancel the zoom update).
// here, we just defer calling [zoomEnd] for 100ms to ensure the update is propagated through.
_scrollTimer = Timer(const Duration(milliseconds: 100), () async {
await widget.controller.zoomEnd();
});
}
Timer? _pickTimer;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Listener( return Listener(
onPointerHover: (event) async { onPointerHover: (event) =>
_gizmo?.checkHover(event.localPosition.dx, event.localPosition.dy); _gestureHandler.onPointerHover(event.localPosition),
}, onPointerSignal: (PointerSignalEvent pointerSignal) {
onPointerSignal: (PointerSignalEvent pointerSignal) async { if (pointerSignal is PointerScrollEvent) {
if (pointerSignal is PointerScrollEvent) { _gestureHandler.onPointerScroll(
if (widget.enableCamera) { pointerSignal.localPosition, pointerSignal.scrollDelta.dy);
_zoom(pointerSignal); }
} },
} else { onPointerPanZoomStart: (pzs) {
throw Exception("TODO"); throw Exception("TODO - is this a pinch zoom on laptop trackpad?");
} },
}, onPointerDown: (d) =>
onPointerPanZoomStart: (pzs) { _gestureHandler.onPointerDown(d.localPosition, d.buttons),
throw Exception("TODO - is this a pinch zoom on laptop trackpad?"); onPointerMove: (d) => _gestureHandler.onPointerMove(
}, d.localPosition, d.delta, d.buttons),
onPointerDown: (d) async { onPointerUp: (d) => _gestureHandler.onPointerUp(d.buttons),
if (d.buttons != kTertiaryButton && widget.enablePicking) { child: widget.child,
widget.controller );
.pick(d.localPosition.dx.toInt(), d.localPosition.dy.toInt());
}
_pointerMoving = false;
},
// holding/moving the left mouse button is interpreted as a pan, middle mouse button as a rotate
onPointerMove: (PointerMoveEvent d) async {
if (_gizmo?.isHovered == true) {
_gizmo!.translate(d.delta.dx, d.delta.dy);
return;
}
// if this is the first move event, we need to call rotateStart/panStart to set the first coordinates
if (!_pointerMoving) {
if (d.buttons == kTertiaryButton && widget.enableCamera) {
widget.controller
.rotateStart(d.localPosition.dx, d.localPosition.dy);
} else if (widget.enableCamera) {
widget.controller
.panStart(d.localPosition.dx, d.localPosition.dy);
}
}
// set the _pointerMoving flag so we don't call rotateStart/panStart on future move events
_pointerMoving = true;
if (d.buttons == kTertiaryButton && widget.enableCamera) {
widget.controller
.rotateUpdate(d.localPosition.dx, d.localPosition.dy);
} else if (widget.enableCamera) {
widget.controller.panUpdate(d.localPosition.dx, d.localPosition.dy);
}
},
// when the left mouse button is released:
// 1) if _pointerMoving is true, this completes the pan
// 2) if _pointerMoving is false, this is interpreted as a pick
// same applies to middle mouse button, but this is ignored as a pick
onPointerUp: (PointerUpEvent d) async {
if (_gizmo?.isHovered == true) {
return;
}
if (d.buttons == kTertiaryButton && widget.enableCamera) {
widget.controller.rotateEnd();
} else {
if (_pointerMoving && widget.enableCamera) {
widget.controller.panEnd();
}
}
_pointerMoving = false;
},
child: widget.child);
} }
} }