diff --git a/thermion_dart/lib/thermion_dart/geometry_helper.dart b/thermion_dart/lib/thermion_dart/geometry_helper.dart index ff4851d7..4ada4971 100644 --- a/thermion_dart/lib/thermion_dart/geometry_helper.dart +++ b/thermion_dart/lib/thermion_dart/geometry_helper.dart @@ -6,6 +6,12 @@ class Geometry { final List 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); } -} \ No newline at end of file + + static Geometry cylinder({double radius = 1.0, double length = 1.0}) { + int segments = 32; + List vertices = []; + List 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 vertices = []; + List 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); + } + } +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/configurable_pan_rotate_gesture_handler.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/configurable_pan_rotate_gesture_handler.dart new file mode 100644 index 00000000..c0aa1afa --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/configurable_pan_rotate_gesture_handler.dart @@ -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 onPointerHover(Offset localPosition) async { + // noop + } + + // @override + // Future onPointerScroll(Offset localPosition, double scrollDelta) async { + // await _zoom(localPosition, scrollDelta); + // } + + // @override + // Future 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 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 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 _handleEntityHighlightedMove(Offset localPosition) async { + // if (_highlightedEntity != null) { + // await viewer.queuePositionUpdateFromViewportCoords( + // _highlightedEntity!, + // localPosition.dx, + // localPosition.dy, + // ); + // } + // } + + // Future _zoom(Offset localPosition, double scrollDelta) async { + // _scrollTimer?.cancel(); + // await viewer.zoomBegin(); + // await viewer.zoomUpdate( + // localPosition.dx, localPosition.dy, scrollDelta > 0 ? 1 : -1); + + // _scrollTimer = Timer(const Duration(milliseconds: 100), () async { + // await viewer.zoomEnd(); + // }); + // } + + @override + void dispose() { + } + + @override + Future get initialized => viewer.initialized; + + @override + GestureAction getActionForType(GestureType type) { + // TODO: implement getActionForType + throw UnimplementedError(); + } + + @override + Future onScaleEnd() { + // TODO: implement onScaleEnd + throw UnimplementedError(); + } + + @override + Future onScaleStart() { + // TODO: implement onScaleStart + throw UnimplementedError(); + } + + @override + Future onScaleUpdate() { + // TODO: implement onScaleUpdate + throw UnimplementedError(); + } + + @override + void setActionForType(GestureType type, GestureAction action) { + // TODO: implement setActionForType + } + + @override + Future onPointerDown(Offset localPosition, int buttons) { + // TODO: implement onPointerDown + throw UnimplementedError(); + } + + @override + Future onPointerMove(Offset localPosition, Offset delta, int buttons) { + // TODO: implement onPointerMove + throw UnimplementedError(); + } + + @override + Future onPointerScroll(Offset localPosition, double scrollDelta) { + // TODO: implement onPointerScroll + throw UnimplementedError(); + } + + @override + Future onPointerUp(int buttons) { + // TODO: implement onPointerUp + throw UnimplementedError(); + } +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/picking_camera_gesture_handler.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/picking_camera_gesture_handler.dart new file mode 100644 index 00000000..bd7488f1 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/picking_camera_gesture_handler.dart @@ -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? _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 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 onPointerScroll(Offset localPosition, double scrollDelta) async { + if(!enableCamera) { + return; + } + if (_currentState == ThermionGestureState.NULL || _currentState == ThermionGestureState.ZOOMING) { + await _zoom(localPosition, scrollDelta); + } + } + + @override + Future 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 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 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 _handleEntityHighlightedMove(Offset localPosition) async { + if (_highlightedEntity != null) { + await viewer.queuePositionUpdateFromViewportCoords( + _highlightedEntity!, + localPosition.dx, + localPosition.dy, + ); + } + } + + Future _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 get initialized => viewer.initialized; + + @override + GestureAction getActionForType(GestureType type) { + // TODO: implement getActionForType + throw UnimplementedError(); + } + + @override + Future onScaleEnd() { + // TODO: implement onScaleEnd + throw UnimplementedError(); + } + + @override + Future onScaleStart() { + // TODO: implement onScaleStart + throw UnimplementedError(); + } + + @override + Future onScaleUpdate() { + // TODO: implement onScaleUpdate + throw UnimplementedError(); + } + + @override + void setActionForType(GestureType type, GestureAction action) { + // TODO: implement setActionForType + } +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_detector.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_detector.dart index 20421c04..ae20650f 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_detector.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_detector.dart @@ -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); + // } + // }); } } diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_detector_desktop.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_detector_desktop.dart index 11110ddd..948b92ab 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_detector_desktop.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_detector_desktop.dart @@ -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 { - 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,11 +46,11 @@ 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, ); } -} \ No newline at end of file +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart index a5f9ec4a..2be95fe3 100644 --- a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_gesture_handler.dart @@ -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? _pickResultSubscription; - - ThermionGestureHandler({ - required this.viewer, - this.enableCamera = true, - this.enablePicking = true, - }) { - try { - _gizmo = viewer.gizmo; - } catch (err) { - _logger.warning( - "Failed to get gizmo. If you are running on WASM, this is expected"); - } - - _pickResultSubscription = viewer.pickResult.listen(_onPickResult); - - // Add keyboard listener - RawKeyboard.instance.addListener(_handleKeyEvent); - } - - void _handleKeyEvent(RawKeyEvent event) { - if (event is RawKeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - _resetToNullState(); - } - } - - void _resetToNullState() async { - // set current state to NULL first, so that any subsequent pointer movements - // won't attempt to translate a deleted entity - _currentState = ThermionGestureState.NULL; - if (_highlightedEntity != null) { - await viewer.removeStencilHighlight(_highlightedEntity!); - _highlightedEntity = null; - } - } - - void _onPickResult(FilamentPickResult result) async { - var targetEntity = await viewer.getAncestor(result.entity) ?? result.entity; - - if (_highlightedEntity != targetEntity) { - if (_highlightedEntity != null) { - await viewer.removeStencilHighlight(_highlightedEntity!); - } - - _highlightedEntity = targetEntity; - if (_highlightedEntity != null) { - await viewer.setStencilHighlight(_highlightedEntity!); - } - - _currentState = _highlightedEntity != null - ? ThermionGestureState.ENTITY_HIGHLIGHTED - : ThermionGestureState.NULL; - } - } - - Future onPointerHover(Offset localPosition) async { - if (_currentState == ThermionGestureState.GIZMO_ATTACHED) { - _gizmo?.checkHover(localPosition.dx, localPosition.dy); - } - - // Update highlighted entity position - if (_highlightedEntity != null) { - await viewer.queuePositionUpdateFromViewportCoords( - _highlightedEntity!, - localPosition.dx, - localPosition.dy, - ); - } - } - - Future onPointerScroll(Offset localPosition, double scrollDelta) async { - if (_currentState == ThermionGestureState.NULL && enableCamera) { - await _zoom(localPosition, scrollDelta); - } - } - - Future onPointerDown(Offset localPosition, int buttons) async { - if (_currentState == ThermionGestureState.ENTITY_HIGHLIGHTED) { - _resetToNullState(); - return; - } - - if (enablePicking && buttons != kMiddleMouseButton) { - viewer.pick(localPosition.dx.toInt(), localPosition.dy.toInt()); - } - - if (buttons == kMiddleMouseButton && enableCamera) { - await viewer.rotateStart(localPosition.dx, localPosition.dy); - _currentState = ThermionGestureState.ROTATING; - } else if (buttons == kPrimaryMouseButton && enableCamera) { - await viewer.panStart(localPosition.dx, localPosition.dy); - _currentState = ThermionGestureState.PANNING; - } - } - - Future onPointerMove( - Offset localPosition, Offset delta, int buttons) async { - switch (_currentState) { - case ThermionGestureState.NULL: - // This case should not occur now, as we set the state on pointer down - break; - case ThermionGestureState.ENTITY_HIGHLIGHTED: - await _handleEntityHighlightedMove(localPosition); - break; - case ThermionGestureState.GIZMO_ATTACHED: - // Do nothing - break; - case ThermionGestureState.ROTATING: - if (enableCamera) { - await viewer.rotateUpdate(localPosition.dx, localPosition.dy); - } - break; - case ThermionGestureState.PANNING: - if (enableCamera) { - await viewer.panUpdate(localPosition.dx, localPosition.dy); - } - break; - } - } - - Future onPointerUp(int buttons) async { - switch (_currentState) { - case ThermionGestureState.ROTATING: - await viewer.rotateEnd(); - _currentState = ThermionGestureState.NULL; - break; - case ThermionGestureState.PANNING: - await viewer.panEnd(); - _currentState = ThermionGestureState.NULL; - break; - default: - // For other states, no action needed - break; - } - } - - Future _handleEntityHighlightedMove(Offset localPosition) async { - if (_highlightedEntity != null) { - await viewer.queuePositionUpdateFromViewportCoords( - _highlightedEntity!, - localPosition.dx, - localPosition.dy, - ); - } - } - - Future _zoom(Offset localPosition, double scrollDelta) async { - _scrollTimer?.cancel(); - await viewer.zoomBegin(); - await viewer.zoomUpdate( - localPosition.dx, localPosition.dy, scrollDelta > 0 ? 1 : -1); - - _scrollTimer = Timer(const Duration(milliseconds: 100), () async { - await viewer.zoomEnd(); - }); - } - - void dispose() { - _pickResultSubscription?.cancel(); - if (_highlightedEntity != null) { - viewer.removeStencilHighlight(_highlightedEntity!); - } - // Remove keyboard listener - RawKeyboard.instance.removeListener(_handleKeyEvent); - } +abstract class ThermionGestureHandler { + GestureAction getActionForType(GestureType type); + void setActionForType(GestureType type, GestureAction action); + Future onPointerHover(Offset localPosition); + Future onPointerScroll(Offset localPosition, double scrollDelta); + Future onPointerDown(Offset localPosition, int buttons); + Future onPointerMove(Offset localPosition, Offset delta, int buttons); + Future onPointerUp(int buttons); + Future onScaleStart(); + Future onScaleUpdate(); + Future onScaleEnd(); + Future 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? _pickResultSubscription; + +// ThermionGestureHandler({ +// required this.viewer, +// this.enableCamera = true, +// this.enablePicking = true, +// }) { +// try { +// _gizmo = viewer.gizmo; +// } catch (err) { +// _logger.warning( +// "Failed to get gizmo. If you are running on WASM, this is expected"); +// } + +// _pickResultSubscription = viewer.pickResult.listen(_onPickResult); + +// // Add keyboard listener +// RawKeyboard.instance.addListener(_handleKeyEvent); +// } + +// void _handleKeyEvent(RawKeyEvent event) { +// if (event is RawKeyDownEvent && +// event.logicalKey == LogicalKeyboardKey.escape) { +// _resetToNullState(); +// } +// } + +// void _resetToNullState() async { +// // set current state to NULL first, so that any subsequent pointer movements +// // won't attempt to translate a deleted entity +// _currentState = ThermionGestureState.NULL; +// if (_highlightedEntity != null) { +// await viewer.removeStencilHighlight(_highlightedEntity!); +// _highlightedEntity = null; +// } +// } + +// void _onPickResult(FilamentPickResult result) async { +// var targetEntity = await viewer.getAncestor(result.entity) ?? result.entity; + +// if (_highlightedEntity != targetEntity) { +// if (_highlightedEntity != null) { +// await viewer.removeStencilHighlight(_highlightedEntity!); +// } + +// _highlightedEntity = targetEntity; +// if (_highlightedEntity != null) { +// await viewer.setStencilHighlight(_highlightedEntity!); +// } + +// _currentState = _highlightedEntity != null +// ? ThermionGestureState.ENTITY_HIGHLIGHTED +// : ThermionGestureState.NULL; +// } +// } + +// Future onPointerHover(Offset localPosition) async { +// if (_currentState == ThermionGestureState.GIZMO_ATTACHED) { +// _gizmo?.checkHover(localPosition.dx, localPosition.dy); +// } + +// // Update highlighted entity position +// if (_highlightedEntity != null) { +// await viewer.queuePositionUpdateFromViewportCoords( +// _highlightedEntity!, +// localPosition.dx, +// localPosition.dy, +// ); +// } +// } + +// Future onPointerScroll(Offset localPosition, double scrollDelta) async { +// if (_currentState == ThermionGestureState.NULL && enableCamera) { +// await _zoom(localPosition, scrollDelta); +// } +// } + +// Future onPointerDown(Offset localPosition, int buttons) async { +// if (_currentState == ThermionGestureState.ENTITY_HIGHLIGHTED) { +// _resetToNullState(); +// return; +// } + +// if (enablePicking && buttons != kMiddleMouseButton) { +// viewer.pick(localPosition.dx.toInt(), localPosition.dy.toInt()); +// } + +// if (buttons == kMiddleMouseButton && enableCamera) { +// await viewer.rotateStart(localPosition.dx, localPosition.dy); +// _currentState = ThermionGestureState.ROTATING; +// } else if (buttons == kPrimaryMouseButton && enableCamera) { +// await viewer.panStart(localPosition.dx, localPosition.dy); +// _currentState = ThermionGestureState.PANNING; +// } +// } + +// Future onPointerMove( +// Offset localPosition, Offset delta, int buttons) async { +// switch (_currentState) { +// case ThermionGestureState.NULL: +// // This case should not occur now, as we set the state on pointer down +// break; +// case ThermionGestureState.ENTITY_HIGHLIGHTED: +// await _handleEntityHighlightedMove(localPosition); +// break; +// case ThermionGestureState.GIZMO_ATTACHED: +// // Do nothing +// break; +// case ThermionGestureState.ROTATING: +// if (enableCamera) { +// await viewer.rotateUpdate(localPosition.dx, localPosition.dy); +// } +// break; +// case ThermionGestureState.PANNING: +// if (enableCamera) { +// await viewer.panUpdate(localPosition.dx, localPosition.dy); +// } +// break; +// } +// } + +// Future onPointerUp(int buttons) async { +// switch (_currentState) { +// case ThermionGestureState.ROTATING: +// await viewer.rotateEnd(); +// _currentState = ThermionGestureState.NULL; +// break; +// case ThermionGestureState.PANNING: +// await viewer.panEnd(); +// _currentState = ThermionGestureState.NULL; +// break; +// default: +// // For other states, no action needed +// break; +// } +// } + +// Future _handleEntityHighlightedMove(Offset localPosition) async { +// if (_highlightedEntity != null) { +// await viewer.queuePositionUpdateFromViewportCoords( +// _highlightedEntity!, +// localPosition.dx, +// localPosition.dy, +// ); +// } +// } + +// Future _zoom(Offset localPosition, double scrollDelta) async { +// _scrollTimer?.cancel(); +// await viewer.zoomBegin(); +// await viewer.zoomUpdate( +// localPosition.dx, localPosition.dy, scrollDelta > 0 ? 1 : -1); + +// _scrollTimer = Timer(const Duration(milliseconds: 100), () async { +// await viewer.zoomEnd(); +// }); +// } + +// void dispose() { +// _pickResultSubscription?.cancel(); +// if (_highlightedEntity != null) { +// viewer.removeStencilHighlight(_highlightedEntity!); +// } +// // Remove keyboard listener +// RawKeyboard.instance.removeListener(_handleKeyEvent); +// } +// } diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_listener_widget.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_listener_widget.dart new file mode 100644 index 00000000..3295c58a --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/thermion_listener_widget.dart @@ -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)) + ]); + }); + } +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/mobile_gesture_handler_selector_widget.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/mobile_gesture_handler_selector_widget.dart new file mode 100644 index 00000000..210ee718 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/mobile_gesture_handler_selector_widget.dart @@ -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), + // ), + // ); + } +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_desktop_widget.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_desktop_widget.dart new file mode 100644 index 00000000..38f5ad0e --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_desktop_widget.dart @@ -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,), + ); + } +} diff --git a/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_mobile_widget.dart b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_mobile_widget.dart new file mode 100644 index 00000000..b6907d96 --- /dev/null +++ b/thermion_flutter/thermion_flutter/lib/thermion/widgets/camera/gestures/v2/thermion_gesture_detector_mobile_widget.dart @@ -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 createState() => _ThermionGestureDetectorMobileState(); +} + +class _ThermionGestureDetectorMobileState + extends State { + @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, + ), + ), + + ]); + } +}