157 lines
5.5 KiB
Dart
157 lines
5.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import '../filament_controller.dart';
|
|
|
|
///
|
|
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
|
|
///
|
|
class FilamentGestureDetectorDesktop extends StatefulWidget {
|
|
///
|
|
/// The content to display below the gesture detector/listener widget.
|
|
/// This will usually be a FilamentWidget (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;
|
|
|
|
///
|
|
/// The [controller] attached to the [FilamentWidget] you wish to control.
|
|
///
|
|
final FilamentController 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;
|
|
|
|
///
|
|
/// If false, gestures will not manipulate the active camera.
|
|
///
|
|
final bool enableCamera;
|
|
|
|
///
|
|
/// If false, pointer down events will not trigger hit-testing (picking).
|
|
///
|
|
final bool enablePicking;
|
|
|
|
const FilamentGestureDetectorDesktop(
|
|
{Key? key,
|
|
required this.controller,
|
|
this.child,
|
|
this.showControlOverlay = false,
|
|
this.enableCamera = true,
|
|
this.enablePicking = true})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _FilamentGestureDetectorDesktopState();
|
|
}
|
|
|
|
class _FilamentGestureDetectorDesktopState
|
|
extends State<FilamentGestureDetectorDesktop> {
|
|
///
|
|
///
|
|
///
|
|
// ignore: unused_field
|
|
final bool _scaling = false;
|
|
|
|
bool _pointerMoving = false;
|
|
|
|
@override
|
|
void didUpdateWidget(FilamentGestureDetectorDesktop oldWidget) {
|
|
if (widget.showControlOverlay != oldWidget.showControlOverlay ||
|
|
widget.enableCamera != oldWidget.enableCamera ||
|
|
widget.enablePicking != oldWidget.enablePicking) {
|
|
setState(() {});
|
|
}
|
|
|
|
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();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Listener(
|
|
onPointerSignal: (PointerSignalEvent pointerSignal) async {
|
|
if (pointerSignal is PointerScrollEvent) {
|
|
if (widget.enableCamera) {
|
|
_zoom(pointerSignal);
|
|
}
|
|
} else {
|
|
throw Exception("TODO");
|
|
}
|
|
},
|
|
onPointerPanZoomStart: (pzs) {
|
|
throw Exception("TODO - is this a pinch zoom on laptop trackpad?");
|
|
},
|
|
// ignore all pointer down events
|
|
// so we can wait to see if the pointer will be held/moved (interpreted as rotate/pan),
|
|
// or if this is a single mousedown event (interpreted as viewport pick)
|
|
onPointerDown: (d) async {
|
|
if (d.buttons != kTertiaryButton && widget.enablePicking) {
|
|
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 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 (d.buttons == kTertiaryButton && widget.enableCamera) {
|
|
widget.controller.rotateEnd();
|
|
} else {
|
|
if (_pointerMoving && widget.enableCamera) {
|
|
widget.controller.panEnd();
|
|
}
|
|
}
|
|
_pointerMoving = false;
|
|
},
|
|
child: widget.child);
|
|
}
|
|
}
|