add v2 gesture handlers

This commit is contained in:
Nick Fisher
2024-09-13 10:34:12 +08:00
parent a5cb23c32e
commit cac80159ff
10 changed files with 895 additions and 256 deletions

View File

@@ -6,6 +6,12 @@ class Geometry {
final List<double> normals;
Geometry(this.vertices, this.indices, this.normals);
void scale(double factor) {
for (int i = 0; i < vertices.length; i++) {
vertices[i] = vertices[i] * factor;
}
}
}
class GeometryHelper {
@@ -32,7 +38,11 @@ class GeometryHelper {
double z = sinPhi * sinTheta;
vertices.addAll([x, y, z]);
normals.addAll([x, y, z]); // For a sphere, normals are the same as vertex positions
normals.addAll([
x,
y,
z
]); // For a sphere, normals are the same as vertex positions
}
}
@@ -41,7 +51,8 @@ class GeometryHelper {
int first = (latNumber * (longitudeBands + 1)) + longNumber;
int second = first + longitudeBands + 1;
indices.addAll([first, second, first + 1, second, second + 1, first + 1]);
indices
.addAll([first, second, first + 1, second, second + 1, first + 1]);
}
}
@@ -142,4 +153,73 @@ class GeometryHelper {
return Geometry(vertices, indices, normals);
}
static Geometry cylinder({double radius = 1.0, double length = 1.0}) {
int segments = 32;
List<double> vertices = [];
List<int> indices = [];
// Create vertices
for (int i = 0; i <= segments; i++) {
double theta = i * 2 * pi / segments;
double x = radius * cos(theta);
double z = radius * sin(theta);
// Top circle
vertices.addAll([x, length / 2, z]);
// Bottom circle
vertices.addAll([x, -length / 2, z]);
}
// Create indices
for (int i = 0; i < segments; i++) {
int topFirst = i * 2;
int topSecond = (i + 1) * 2;
int bottomFirst = topFirst + 1;
int bottomSecond = topSecond + 1;
// Top face (counter-clockwise)
indices.addAll([segments * 2, topSecond, topFirst]);
// Bottom face (counter-clockwise when viewed from below)
indices.addAll([segments * 2 + 1, bottomFirst, bottomSecond]);
// Side faces (counter-clockwise)
indices.addAll([topFirst, bottomFirst, topSecond]);
indices.addAll([bottomFirst, bottomSecond, topSecond]);
}
// Add center vertices for top and bottom faces
vertices.addAll([0, length / 2, 0]); // Top center
vertices.addAll([0, -length / 2, 0]); // Bottom center
return Geometry(vertices:vertices, indices:indices, normals:normals);
}
static Geometry conic({double radius = 1.0, double length = 1.0}) {
int segments = 32;
List<double> vertices = [];
List<int> indices = [];
// Create vertices
for (int i = 0; i <= segments; i++) {
double theta = i * 2 * pi / segments;
double x = radius * cos(theta);
double z = radius * sin(theta);
// Base circle
vertices.addAll([x, 0, z]);
}
// Apex
vertices.addAll([0, length, 0]);
// Create indices
for (int i = 0; i < segments; i++) {
// Base face
indices.addAll([i, i + 1, segments + 1]);
// Side faces
indices.addAll([i, segments, i + 1]);
}
return Geometry(vertices:vertices, indices:indices, normals:normals);
}
}
}

View File

@@ -0,0 +1,155 @@
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<void> onPointerHover(Offset localPosition) async {
// noop
}
// @override
// Future<void> onPointerScroll(Offset localPosition, double scrollDelta) async {
// await _zoom(localPosition, scrollDelta);
// }
// @override
// Future<void> 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<void> 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<void> 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<void> _handleEntityHighlightedMove(Offset localPosition) async {
// if (_highlightedEntity != null) {
// await viewer.queuePositionUpdateFromViewportCoords(
// _highlightedEntity!,
// localPosition.dx,
// localPosition.dy,
// );
// }
// }
// Future<void> _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<bool> get initialized => viewer.initialized;
@override
GestureAction getActionForType(GestureType type) {
// TODO: implement getActionForType
throw UnimplementedError();
}
@override
Future<void> onScaleEnd() {
// TODO: implement onScaleEnd
throw UnimplementedError();
}
@override
Future<void> onScaleStart() {
// TODO: implement onScaleStart
throw UnimplementedError();
}
@override
Future<void> onScaleUpdate() {
// TODO: implement onScaleUpdate
throw UnimplementedError();
}
@override
void setActionForType(GestureType type, GestureAction action) {
// TODO: implement setActionForType
}
@override
Future<void> onPointerDown(Offset localPosition, int buttons) {
// TODO: implement onPointerDown
throw UnimplementedError();
}
@override
Future<void> onPointerMove(Offset localPosition, Offset delta, int buttons) {
// TODO: implement onPointerMove
throw UnimplementedError();
}
@override
Future<void> onPointerScroll(Offset localPosition, double scrollDelta) {
// TODO: implement onPointerScroll
throw UnimplementedError();
}
@override
Future<void> onPointerUp(int buttons) {
// TODO: implement onPointerUp
throw UnimplementedError();
}
}

View File

@@ -0,0 +1,230 @@
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';
import 'dart:ui';
import 'package:thermion_flutter/thermion/widgets/camera/gestures/thermion_gesture_handler.dart';
// Renamed implementation
class PickingCameraGestureHandler implements ThermionGestureHandler {
final ThermionViewer viewer;
final bool enableCamera;
final bool enablePicking;
final Logger _logger = Logger("PickingCameraGestureHandler");
ThermionGestureState _currentState = ThermionGestureState.NULL;
AbstractGizmo? _gizmo;
Timer? _scrollTimer;
ThermionEntity? _highlightedEntity;
StreamSubscription<FilamentPickResult>? _pickResultSubscription;
bool _gizmoAttached = false;
PickingCameraGestureHandler({
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);
}
@override
ThermionGestureState get currentState => _currentState;
void _handleKeyEvent(RawKeyEvent event) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
_resetToNullState();
}
}
void _resetToNullState() async {
_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!);
}
}
}
@override
Future<void> onPointerHover(Offset localPosition) async {
if (_gizmoAttached) {
_gizmo?.checkHover(localPosition.dx, localPosition.dy);
}
if (_highlightedEntity != null) {
await viewer.queuePositionUpdateFromViewportCoords(
_highlightedEntity!,
localPosition.dx,
localPosition.dy,
);
}
}
@override
Future<void> onPointerScroll(Offset localPosition, double scrollDelta) async {
if(!enableCamera) {
return;
}
if (_currentState == ThermionGestureState.NULL || _currentState == ThermionGestureState.ZOOMING) {
await _zoom(localPosition, scrollDelta);
}
}
@override
Future<void> onPointerDown(Offset localPosition, int buttons) async {
if (_highlightedEntity != null) {
_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;
}
}
@override
Future<void> onPointerMove(
Offset localPosition, Offset delta, int buttons) async {
if (_highlightedEntity != null) {
await _handleEntityHighlightedMove(localPosition);
return;
}
switch (_currentState) {
case ThermionGestureState.NULL:
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;
case ThermionGestureState.ZOOMING:
// ignore
break;
}
}
@override
Future<void> 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<void> _handleEntityHighlightedMove(Offset localPosition) async {
if (_highlightedEntity != null) {
await viewer.queuePositionUpdateFromViewportCoords(
_highlightedEntity!,
localPosition.dx,
localPosition.dy,
);
}
}
Future<void> _zoom(Offset localPosition, double scrollDelta) async {
_scrollTimer?.cancel();
_currentState = ThermionGestureState.ZOOMING;
await viewer.zoomBegin();
await viewer.zoomUpdate(
localPosition.dx, localPosition.dy, scrollDelta > 0 ? 1 : -1);
_scrollTimer = Timer(const Duration(milliseconds: 100), () async {
await viewer.zoomEnd();
_currentState = ThermionGestureState.NULL;
});
}
@override
void dispose() {
_pickResultSubscription?.cancel();
if (_highlightedEntity != null) {
viewer.removeStencilHighlight(_highlightedEntity!);
}
RawKeyboard.instance.removeListener(_handleKeyEvent);
}
@override
Future<bool> get initialized => viewer.initialized;
@override
GestureAction getActionForType(GestureType type) {
// TODO: implement getActionForType
throw UnimplementedError();
}
@override
Future<void> onScaleEnd() {
// TODO: implement onScaleEnd
throw UnimplementedError();
}
@override
Future<void> onScaleStart() {
// TODO: implement onScaleStart
throw UnimplementedError();
}
@override
Future<void> onScaleUpdate() {
// TODO: implement onScaleUpdate
throw UnimplementedError();
}
@override
void setActionForType(GestureType type, GestureAction action) {
// TODO: implement setActionForType
}
}

View File

@@ -1,28 +1,22 @@
import 'dart:io';
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'thermion_gesture_detector_desktop.dart';
import 'thermion_gesture_detector_mobile.dart';
enum GestureType { rotateCamera, panCamera, panBackground }
///
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
///
@Deprecated("Use ThermionListenerWidget instead")
class ThermionGestureDetector extends StatelessWidget {
///
/// 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 [Filamentviewer].
/// 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 [viewer] attached to the [ThermionWidget] you wish to control.
/// The [controller] attached to the [ThermionWidget] you wish to control.
///
final ThermionViewer viewer;
final ThermionViewer controller;
///
/// If true, an overlay will be shown with buttons to toggle whether pointer movements are interpreted as:
@@ -47,7 +41,7 @@ class ThermionGestureDetector extends StatelessWidget {
const ThermionGestureDetector(
{Key? key,
required this.viewer,
required this.controller,
this.child,
this.showControlOverlay = false,
this.enableCamera = true,
@@ -59,33 +53,34 @@ class ThermionGestureDetector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: viewer.initialized,
builder: (_, initialized) {
if (initialized.data != true) {
return child ?? Container();
}
if (kIsWeb || Platform.isLinux ||
Platform.isWindows ||
Platform.isMacOS) {
return ThermionGestureDetectorDesktop(
viewer: viewer,
child: child,
showControlOverlay: showControlOverlay,
enableCamera: enableCamera,
enablePicking: enablePicking,
);
} else {
return ThermionGestureDetectorMobile(
viewer: viewer,
child: child,
showControlOverlay: showControlOverlay,
enableCamera: enableCamera,
enablePicking: enablePicking,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd);
}
});
throw Exception("TODO");
// return FutureBuilder(
// future: controller.initialized,
// builder: (_, initialized) {
// if (initialized.data != true) {
// return child ?? Container();
// }
// if (kIsWeb || Platform.isLinux ||
// Platform.isWindows ||
// Platform.isMacOS) {
// return ThermionGestureDetectorDesktop(
// controller: controller,
// child: child,
// showControlOverlay: showControlOverlay,
// enableCamera: enableCamera,
// enablePicking: enablePicking,
// );
// } else {
// return ThermionGestureDetectorMobile(
// controller: controller,
// child: child,
// showControlOverlay: showControlOverlay,
// enableCamera: enableCamera,
// enablePicking: enablePicking,
// onScaleStart: onScaleStart,
// onScaleUpdate: onScaleUpdate,
// onScaleEnd: onScaleEnd);
// }
// });
}
}

View File

@@ -10,14 +10,14 @@ import 'package:vector_math/vector_math_64.dart' as v64;
class ThermionGestureDetectorDesktop extends StatefulWidget {
final Widget? child;
final ThermionViewer viewer;
final ThermionGestureHandler gestureHandler;
final bool showControlOverlay;
final bool enableCamera;
final bool enablePicking;
const ThermionGestureDetectorDesktop({
Key? key,
required this.viewer,
required this.gestureHandler,
this.child,
this.showControlOverlay = false,
this.enableCamera = true,
@@ -30,38 +30,15 @@ class ThermionGestureDetectorDesktop extends StatefulWidget {
class _ThermionGestureDetectorDesktopState
extends State<ThermionGestureDetectorDesktop> {
late ThermionGestureHandler _gestureHandler;
@override
void initState() {
super.initState();
_gestureHandler = ThermionGestureHandler(
enableCamera: widget.enableCamera,
enablePicking: widget.enablePicking, viewer: widget.viewer,
);
}
@override
void didUpdateWidget(ThermionGestureDetectorDesktop oldWidget) {
if (widget.enableCamera != oldWidget.enableCamera ||
widget.enablePicking != oldWidget.enablePicking) {
_gestureHandler = ThermionGestureHandler(
viewer: widget.viewer,
enableCamera: widget.enableCamera,
enablePicking: widget.enablePicking,
);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerHover: (event) =>
_gestureHandler.onPointerHover(event.localPosition),
widget.gestureHandler.onPointerHover(event.localPosition),
onPointerSignal: (PointerSignalEvent pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
_gestureHandler.onPointerScroll(
widget.gestureHandler.onPointerScroll(
pointerSignal.localPosition, pointerSignal.scrollDelta.dy);
}
},
@@ -69,10 +46,10 @@ class _ThermionGestureDetectorDesktopState
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),
widget.gestureHandler.onPointerDown(d.localPosition, d.buttons),
onPointerMove: (d) =>
widget.gestureHandler.onPointerMove(d.localPosition, d.delta, d.buttons),
onPointerUp: (d) => widget.gestureHandler.onPointerUp(d.buttons),
child: widget.child,
);
}

View File

@@ -2,192 +2,221 @@ 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 GestureType { POINTER_HOVER, POINTER_DOWN, SCALE }
enum GestureAction {
PAN_CAMERA,
ROTATE_CAMERA,
ZOOM_CAMERA,
TRANSLATE_ENTITY,
ROTATE_ENTITY
}
enum ThermionGestureState {
NULL,
ENTITY_HIGHLIGHTED,
GIZMO_ATTACHED,
ROTATING,
PANNING,
ZOOMING, // aka SCROLL
}
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<FilamentPickResult>? _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<void> 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<void> onPointerScroll(Offset localPosition, double scrollDelta) async {
if (_currentState == ThermionGestureState.NULL && enableCamera) {
await _zoom(localPosition, scrollDelta);
}
}
Future<void> 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<void> 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<void> 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<void> _handleEntityHighlightedMove(Offset localPosition) async {
if (_highlightedEntity != null) {
await viewer.queuePositionUpdateFromViewportCoords(
_highlightedEntity!,
localPosition.dx,
localPosition.dy,
);
}
}
Future<void> _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);
}
abstract class ThermionGestureHandler {
GestureAction getActionForType(GestureType type);
void setActionForType(GestureType type, GestureAction action);
Future<void> onPointerHover(Offset localPosition);
Future<void> onPointerScroll(Offset localPosition, double scrollDelta);
Future<void> onPointerDown(Offset localPosition, int buttons);
Future<void> onPointerMove(Offset localPosition, Offset delta, int buttons);
Future<void> onPointerUp(int buttons);
Future<void> onScaleStart();
Future<void> onScaleUpdate();
Future<void> onScaleEnd();
Future<bool> get initialized;
void dispose();
}
// 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<FilamentPickResult>? _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<void> 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<void> onPointerScroll(Offset localPosition, double scrollDelta) async {
// if (_currentState == ThermionGestureState.NULL && enableCamera) {
// await _zoom(localPosition, scrollDelta);
// }
// }
// Future<void> 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<void> 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<void> 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<void> _handleEntityHighlightedMove(Offset localPosition) async {
// if (_highlightedEntity != null) {
// await viewer.queuePositionUpdateFromViewportCoords(
// _highlightedEntity!,
// localPosition.dx,
// localPosition.dy,
// );
// }
// }
// Future<void> _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);
// }
// }

View File

@@ -0,0 +1,57 @@
import 'dart:io';
import 'package:flutter/foundation.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].
///
class ThermionListenerWidget extends StatelessWidget {
///
/// 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 [FilamentViewer].
///
final Widget? child;
///
/// The handler to use for interpreting gestures/pointer movements.
///
final ThermionGestureHandler gestureHandler;
ThermionListenerWidget({
Key? key,
required this.gestureHandler,
this.child,
}) : super(key: key);
bool get isDesktop => kIsWeb ||
Platform.isLinux ||
Platform.isWindows ||
Platform.isMacOS;
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: gestureHandler.initialized,
builder: (_, initialized) {
if (initialized.data != true) {
return child ?? Container();
}
return Stack(children: [
if(child != null)
Positioned.fill(child:child!),
if (isDesktop)
Positioned.fill(child:ThermionGestureDetectorDesktop(
gestureHandler: gestureHandler)),
if(!isDesktop)
Positioned.fill(child:ThermionGestureDetectorMobile(
gestureHandler: gestureHandler))
]);
});
}
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter/widgets.dart';
import 'package:thermion_flutter/thermion/widgets/camera/gestures/thermion_gesture_handler.dart';
class MobileGestureHandlerSelectorWidget extends StatelessWidget {
final ThermionGestureHandler handler;
const MobileGestureHandlerSelectorWidget({super.key, required this.handler});
@override
Widget build(BuildContext context) {
throw Exception("TODO");
// return GestureDetector(
// onTap: () {
// var curIdx =
// GestureType.values.indexOf(handler.gestureType);
// var nextIdx =
// curIdx == GestureType.values.length - 1 ? 0 : curIdx + 1;
// handler.setGestureType(GestureType.values[nextIdx]);
// });
// },
// child: Container(
// padding: const EdgeInsets.all(50),
// child: Icon(_icons[widget.gestureHandler.gestureType],
// color: Colors.green),
// ),
// );
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:thermion_flutter/thermion/widgets/camera/gestures/thermion_gesture_handler.dart';
class ThermionGestureDetectorDesktop extends StatelessWidget {
final ThermionGestureHandler gestureHandler;
const ThermionGestureDetectorDesktop({
Key? key,
required this.gestureHandler,
}) : super(key: key);
@override
Widget build(BuildContext context) {
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: Container(color: Colors.transparent,),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/widgets.dart';
import 'package:thermion_flutter/thermion/widgets/camera/gestures/thermion_gesture_handler.dart';
class ThermionGestureDetectorMobile extends StatefulWidget {
final Widget? child;
final ThermionGestureHandler gestureHandler;
const ThermionGestureDetectorMobile({
Key? key,
required this.gestureHandler,
this.child,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _ThermionGestureDetectorMobileState();
}
class _ThermionGestureDetectorMobileState
extends State<ThermionGestureDetectorMobile> {
@override
Widget build(BuildContext context) {
return Stack(children: [
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
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);
} else {
widget.gestureHandler.setActionForType(GestureType.SCALE, GestureAction.PAN_CAMERA);
}
},
onScaleStart: (details) async {
await widget.gestureHandler.onScaleStart();
},
onScaleUpdate: (details) async {
await widget.gestureHandler.onScaleUpdate();
},
onScaleEnd: (details) async {
await widget.gestureHandler.onScaleUpdate();
},
child: widget.child,
),
),
]);
}
}