add v2 gesture handlers
This commit is contained in:
@@ -6,6 +6,12 @@ class Geometry {
|
|||||||
final List<double> normals;
|
final List<double> normals;
|
||||||
|
|
||||||
Geometry(this.vertices, this.indices, this.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 {
|
class GeometryHelper {
|
||||||
@@ -32,7 +38,11 @@ class GeometryHelper {
|
|||||||
double z = sinPhi * sinTheta;
|
double z = sinPhi * sinTheta;
|
||||||
|
|
||||||
vertices.addAll([x, y, z]);
|
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 first = (latNumber * (longitudeBands + 1)) + longNumber;
|
||||||
int second = first + longitudeBands + 1;
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,22 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.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.
|
/// A widget that translates finger/mouse gestures to zoom/pan/rotate actions.
|
||||||
///
|
///
|
||||||
|
@Deprecated("Use ThermionListenerWidget instead")
|
||||||
class ThermionGestureDetector extends StatelessWidget {
|
class ThermionGestureDetector extends StatelessWidget {
|
||||||
///
|
///
|
||||||
/// The content to display below the gesture detector/listener widget.
|
/// 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.
|
/// 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;
|
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:
|
/// 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(
|
const ThermionGestureDetector(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required this.viewer,
|
required this.controller,
|
||||||
this.child,
|
this.child,
|
||||||
this.showControlOverlay = false,
|
this.showControlOverlay = false,
|
||||||
this.enableCamera = true,
|
this.enableCamera = true,
|
||||||
@@ -59,33 +53,34 @@ class ThermionGestureDetector extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder(
|
throw Exception("TODO");
|
||||||
future: viewer.initialized,
|
// return FutureBuilder(
|
||||||
builder: (_, initialized) {
|
// future: controller.initialized,
|
||||||
if (initialized.data != true) {
|
// builder: (_, initialized) {
|
||||||
return child ?? Container();
|
// if (initialized.data != true) {
|
||||||
}
|
// return child ?? Container();
|
||||||
if (kIsWeb || Platform.isLinux ||
|
// }
|
||||||
Platform.isWindows ||
|
// if (kIsWeb || Platform.isLinux ||
|
||||||
Platform.isMacOS) {
|
// Platform.isWindows ||
|
||||||
return ThermionGestureDetectorDesktop(
|
// Platform.isMacOS) {
|
||||||
viewer: viewer,
|
// return ThermionGestureDetectorDesktop(
|
||||||
child: child,
|
// controller: controller,
|
||||||
showControlOverlay: showControlOverlay,
|
// child: child,
|
||||||
enableCamera: enableCamera,
|
// showControlOverlay: showControlOverlay,
|
||||||
enablePicking: enablePicking,
|
// enableCamera: enableCamera,
|
||||||
);
|
// enablePicking: enablePicking,
|
||||||
} else {
|
// );
|
||||||
return ThermionGestureDetectorMobile(
|
// } else {
|
||||||
viewer: viewer,
|
// return ThermionGestureDetectorMobile(
|
||||||
child: child,
|
// controller: controller,
|
||||||
showControlOverlay: showControlOverlay,
|
// child: child,
|
||||||
enableCamera: enableCamera,
|
// showControlOverlay: showControlOverlay,
|
||||||
enablePicking: enablePicking,
|
// enableCamera: enableCamera,
|
||||||
onScaleStart: onScaleStart,
|
// enablePicking: enablePicking,
|
||||||
onScaleUpdate: onScaleUpdate,
|
// onScaleStart: onScaleStart,
|
||||||
onScaleEnd: onScaleEnd);
|
// onScaleUpdate: onScaleUpdate,
|
||||||
}
|
// onScaleEnd: onScaleEnd);
|
||||||
});
|
// }
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import 'package:vector_math/vector_math_64.dart' as v64;
|
|||||||
|
|
||||||
class ThermionGestureDetectorDesktop extends StatefulWidget {
|
class ThermionGestureDetectorDesktop extends StatefulWidget {
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
final ThermionViewer viewer;
|
final ThermionGestureHandler gestureHandler;
|
||||||
final bool showControlOverlay;
|
final bool showControlOverlay;
|
||||||
final bool enableCamera;
|
final bool enableCamera;
|
||||||
final bool enablePicking;
|
final bool enablePicking;
|
||||||
|
|
||||||
const ThermionGestureDetectorDesktop({
|
const ThermionGestureDetectorDesktop({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.viewer,
|
required this.gestureHandler,
|
||||||
this.child,
|
this.child,
|
||||||
this.showControlOverlay = false,
|
this.showControlOverlay = false,
|
||||||
this.enableCamera = true,
|
this.enableCamera = true,
|
||||||
@@ -30,38 +30,15 @@ class ThermionGestureDetectorDesktop extends StatefulWidget {
|
|||||||
|
|
||||||
class _ThermionGestureDetectorDesktopState
|
class _ThermionGestureDetectorDesktopState
|
||||||
extends State<ThermionGestureDetectorDesktop> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerHover: (event) =>
|
onPointerHover: (event) =>
|
||||||
_gestureHandler.onPointerHover(event.localPosition),
|
widget.gestureHandler.onPointerHover(event.localPosition),
|
||||||
onPointerSignal: (PointerSignalEvent pointerSignal) {
|
onPointerSignal: (PointerSignalEvent pointerSignal) {
|
||||||
if (pointerSignal is PointerScrollEvent) {
|
if (pointerSignal is PointerScrollEvent) {
|
||||||
_gestureHandler.onPointerScroll(
|
widget.gestureHandler.onPointerScroll(
|
||||||
pointerSignal.localPosition, pointerSignal.scrollDelta.dy);
|
pointerSignal.localPosition, pointerSignal.scrollDelta.dy);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -69,11 +46,11 @@ class _ThermionGestureDetectorDesktopState
|
|||||||
throw Exception("TODO - is this a pinch zoom on laptop trackpad?");
|
throw Exception("TODO - is this a pinch zoom on laptop trackpad?");
|
||||||
},
|
},
|
||||||
onPointerDown: (d) =>
|
onPointerDown: (d) =>
|
||||||
_gestureHandler.onPointerDown(d.localPosition, d.buttons),
|
widget.gestureHandler.onPointerDown(d.localPosition, d.buttons),
|
||||||
onPointerMove: (d) => _gestureHandler.onPointerMove(
|
onPointerMove: (d) =>
|
||||||
d.localPosition, d.delta, d.buttons),
|
widget.gestureHandler.onPointerMove(d.localPosition, d.delta, d.buttons),
|
||||||
onPointerUp: (d) => _gestureHandler.onPointerUp(d.buttons),
|
onPointerUp: (d) => widget.gestureHandler.onPointerUp(d.buttons),
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,192 +2,221 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:thermion_dart/thermion_dart/entities/abstract_gizmo.dart';
|
enum GestureType { POINTER_HOVER, POINTER_DOWN, SCALE }
|
||||||
import 'package:thermion_dart/thermion_dart/thermion_viewer.dart';
|
|
||||||
|
enum GestureAction {
|
||||||
|
PAN_CAMERA,
|
||||||
|
ROTATE_CAMERA,
|
||||||
|
ZOOM_CAMERA,
|
||||||
|
TRANSLATE_ENTITY,
|
||||||
|
ROTATE_ENTITY
|
||||||
|
}
|
||||||
|
|
||||||
enum ThermionGestureState {
|
enum ThermionGestureState {
|
||||||
NULL,
|
NULL,
|
||||||
ENTITY_HIGHLIGHTED,
|
|
||||||
GIZMO_ATTACHED,
|
|
||||||
ROTATING,
|
ROTATING,
|
||||||
PANNING,
|
PANNING,
|
||||||
|
ZOOMING, // aka SCROLL
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThermionGestureHandler {
|
abstract class ThermionGestureHandler {
|
||||||
final ThermionViewer viewer;
|
GestureAction getActionForType(GestureType type);
|
||||||
final bool enableCamera;
|
void setActionForType(GestureType type, GestureAction action);
|
||||||
final bool enablePicking;
|
Future<void> onPointerHover(Offset localPosition);
|
||||||
final Logger _logger = Logger("ThermionGestureHandler");
|
Future<void> onPointerScroll(Offset localPosition, double scrollDelta);
|
||||||
|
Future<void> onPointerDown(Offset localPosition, int buttons);
|
||||||
ThermionGestureState _currentState = ThermionGestureState.NULL;
|
Future<void> onPointerMove(Offset localPosition, Offset delta, int buttons);
|
||||||
AbstractGizmo? _gizmo;
|
Future<void> onPointerUp(int buttons);
|
||||||
Timer? _scrollTimer;
|
Future<void> onScaleStart();
|
||||||
ThermionEntity? _highlightedEntity;
|
Future<void> onScaleUpdate();
|
||||||
StreamSubscription<FilamentPickResult>? _pickResultSubscription;
|
Future<void> onScaleEnd();
|
||||||
|
Future<bool> get initialized;
|
||||||
ThermionGestureHandler({
|
void dispose();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -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))
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user