From 1ddeac2d31383e7fcc3e357b43e5b926cf5292df Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 9 May 2025 11:18:07 +0800 Subject: [PATCH] refactor InputHandler interface/DelegateInputHandler implementation --- thermion_dart/lib/src/input/input.dart | 2 +- .../src/input/src/delegate_input_handler.dart | 318 ++------ .../lib/src/input/src/delegates.dart | 28 - .../custom_camera_delegate_v2.dart | 228 ++++++ .../default_pick_delegate.dart | 36 +- .../fixed_orbit_camera_delegate_v2.dart | 235 ++++++ .../fixed_orbit_camera_rotation_delegate.dart | 227 +++--- .../free_flight_camera_delegate.dart | 222 +++--- .../free_flight_camera_delegate_v2.dart | 119 +++ .../implementations/gizmo_input_handler.dart | 742 +++++++++--------- .../third_person_camera_delegate.dart | 195 +++-- .../lib/src/input/src/input_handler.dart | 84 +- .../lib/src/input/src/input_types.dart | 92 +++ 13 files changed, 1470 insertions(+), 1058 deletions(-) delete mode 100644 thermion_dart/lib/src/input/src/delegates.dart create mode 100644 thermion_dart/lib/src/input/src/implementations/custom_camera_delegate_v2.dart create mode 100644 thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_delegate_v2.dart create mode 100644 thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate_v2.dart create mode 100644 thermion_dart/lib/src/input/src/input_types.dart diff --git a/thermion_dart/lib/src/input/input.dart b/thermion_dart/lib/src/input/input.dart index 9f9bbc4a..83052172 100644 --- a/thermion_dart/lib/src/input/input.dart +++ b/thermion_dart/lib/src/input/input.dart @@ -1,7 +1,7 @@ library; +export 'src/input_types.dart'; export 'src/input_handler.dart'; -export 'src/delegates.dart'; export 'src/delegate_input_handler.dart'; export 'src/implementations/default_pick_delegate.dart'; export 'src/implementations/gizmo_pick_delegate.dart'; diff --git a/thermion_dart/lib/src/input/src/delegate_input_handler.dart b/thermion_dart/lib/src/input/src/delegate_input_handler.dart index a67e18ce..e95881dc 100644 --- a/thermion_dart/lib/src/input/src/delegate_input_handler.dart +++ b/thermion_dart/lib/src/input/src/delegate_input_handler.dart @@ -1,301 +1,99 @@ import 'dart:async'; import 'package:logging/logging.dart'; +import 'package:thermion_dart/src/input/src/implementations/fixed_orbit_camera_delegate_v2.dart'; +import 'package:thermion_dart/src/input/src/implementations/free_flight_camera_delegate_v2.dart'; import 'package:thermion_dart/thermion_dart.dart'; import 'implementations/fixed_orbit_camera_rotation_delegate.dart'; import 'implementations/free_flight_camera_delegate.dart'; +typedef PointerEventDetails = (Vector2 localPosition, Vector2 delta); + +abstract class InputHandlerDelegate { + Future handle(Set events); +} + +/// +/// An [InputHandler] that accumulates pointer/key events every frame, +/// delegating the actual update to an [InputHandlerDelegate]. +/// class DelegateInputHandler implements InputHandler { final ThermionViewer viewer; - Stream> get gestures => _gesturesController.stream; - final _gesturesController = StreamController>.broadcast(); + late final _logger = Logger(this.runtimeType.toString()); - Stream get cameraUpdated => _cameraUpdatedController.stream; - final _cameraUpdatedController = StreamController.broadcast(); + Stream> get events => _gesturesController.stream; - final _logger = Logger("DelegateInputHandler"); - - InputHandlerDelegate? transformDelegate; - PickDelegate? pickDelegate; - - final Set _pressedKeys = {}; - - final _inputDeltas = {}; - - Map _actions = { - InputType.LMB_HOLD_AND_MOVE: InputAction.TRANSLATE, - InputType.SCALE1: InputAction.TRANSLATE, - InputType.SCALE2: InputAction.ZOOM, - InputType.MMB_HOLD_AND_MOVE: InputAction.ROTATE, - InputType.SCROLLWHEEL: InputAction.TRANSLATE, - InputType.POINTER_MOVE: InputAction.NONE, - InputType.KEYDOWN_W: InputAction.TRANSLATE, - InputType.KEYDOWN_S: InputAction.TRANSLATE, - InputType.KEYDOWN_A: InputAction.TRANSLATE, - InputType.KEYDOWN_D: InputAction.TRANSLATE, - }; - - final _axes = {}; - - void setTransformForAction(InputType inputType, Matrix3 transform) { - _axes[inputType] = transform; - } - - DelegateInputHandler({ - required this.viewer, - required this.transformDelegate, - this.pickDelegate, - Map? actions, - }) { - if (actions != null) { - _actions = actions; - } - - if (pickDelegate != null) { - if (_actions[InputType.LMB_DOWN] != null) { - throw Exception(); - } - _actions[InputType.LMB_DOWN] = InputAction.PICK; - } - - for (var gestureType in InputType.values) { - _inputDeltas[gestureType] = Vector3.zero(); - } + final _gesturesController = StreamController>.broadcast(); + final _events = {}; + final List delegates; + DelegateInputHandler({required this.viewer, required this.delegates}) { FilamentApp.instance!.registerRequestFrameHook(process); } factory DelegateInputHandler.fixedOrbit(ThermionViewer viewer, - {double minimumDistance = 10.0, - Vector3? target, - ThermionEntity? entity, - PickDelegate? pickDelegate}) => - DelegateInputHandler( - viewer: viewer, - pickDelegate: pickDelegate, - transformDelegate: FixedOrbitRotateInputHandlerDelegate(viewer.view, - minimumDistance: minimumDistance), - actions: { - InputType.MMB_HOLD_AND_MOVE: InputAction.ROTATE, - InputType.SCALE1: InputAction.ROTATE, - InputType.SCALE2: InputAction.ZOOM, - InputType.SCROLLWHEEL: InputAction.ZOOM - }); + {double minimumDistance = 0.1, + Vector3? target, + InputSensitivityOptions sensitivity = const InputSensitivityOptions(), + ThermionEntity? entity}) { + return DelegateInputHandler(viewer: viewer, delegates: [ + OrbitInputHandlerDelegate(viewer.view, + sensitivity: sensitivity, + minZoomDistance: minimumDistance, + maxZoomDistance: 1000.0) + ]); + } factory DelegateInputHandler.flight(ThermionViewer viewer, - {PickDelegate? pickDelegate, - bool freeLook = false, - double panSensitivity = 0.1, - double zoomSensitivity = 0.1, - double movementSensitivity = 0.1, - double rotateSensitivity = 0.01, - double? clampY, + {bool freeLook = false, + InputSensitivityOptions sensitivity = const InputSensitivityOptions(), ThermionEntity? entity}) => - DelegateInputHandler( - viewer: viewer, - pickDelegate: pickDelegate, - transformDelegate: FreeFlightInputHandlerDelegate(viewer.view, - clampY: clampY, - rotationSensitivity: rotateSensitivity, - zoomSensitivity: zoomSensitivity, - panSensitivity: panSensitivity, - movementSensitivity: movementSensitivity), - actions: { - InputType.MMB_HOLD_AND_MOVE: InputAction.ROTATE, - InputType.SCROLLWHEEL: InputAction.ZOOM, - InputType.LMB_HOLD_AND_MOVE: InputAction.TRANSLATE, - InputType.KEYDOWN_A: InputAction.TRANSLATE, - InputType.KEYDOWN_W: InputAction.TRANSLATE, - InputType.KEYDOWN_S: InputAction.TRANSLATE, - InputType.KEYDOWN_D: InputAction.TRANSLATE, - InputType.SCALE1: InputAction.TRANSLATE, - InputType.SCALE2: InputAction.ZOOM, - if (freeLook) InputType.POINTER_MOVE: InputAction.ROTATE, - }); + DelegateInputHandler(viewer: viewer, delegates: [ + FreeFlightInputHandlerDelegateV2(viewer.view, sensitivity: sensitivity) + ]); bool _processing = false; + Future process() async { _processing = true; - for (var gestureType in _inputDeltas.keys) { - var vector = _inputDeltas[gestureType]!; - var action = _actions[gestureType]; - if (action == null) { - continue; - } - final transform = _axes[gestureType]; - if (transform != null) { - vector = transform * vector; - } - await transformDelegate?.queue(action, vector); - } - final keyTypes = []; - for (final key in _pressedKeys) { - InputAction? keyAction; - InputType? keyType = null; - Vector3? vector; + final delegate = delegates.first; + final keyUp = {}; + final keyDown = {}; - switch (key) { - case PhysicalKey.W: - keyType = InputType.KEYDOWN_W; - vector = Vector3(0, 0, -1); - break; - case PhysicalKey.A: - keyType = InputType.KEYDOWN_A; - vector = Vector3(-1, 0, 0); - break; - case PhysicalKey.S: - keyType = InputType.KEYDOWN_S; - vector = Vector3(0, 0, 1); - break; - case PhysicalKey.D: - keyType = InputType.KEYDOWN_D; - vector = Vector3(1, 0, 0); - break; - } - - // ignore: unnecessary_null_comparison - if (keyType != null) { - keyAction = _actions[keyType]; - - if (keyAction != null) { - var transform = _axes[keyAction]; - if (transform != null) { - vector = transform * vector; - } - transformDelegate?.queue(keyAction, vector!); - keyTypes.add(keyType); + for (final event in _events) { + if (event is KeyEvent) { + switch (event.type) { + case KeyEventType.up: + keyUp[event.key] = event; + case KeyEventType.down: + keyDown[event.key] = event; } - } + } + } + for (final key in keyUp.keys) { + _events.remove(keyDown[key]); + _events.remove(keyUp[key]); } - var transform = await transformDelegate?.execute(); - var updates = _inputDeltas.keys.followedBy(keyTypes).toList(); - if (updates.isNotEmpty) { - _gesturesController.add(updates); - } - if (transform != null) { - _cameraUpdatedController.add(transform); - } + await delegate.handle(_events); - _inputDeltas.clear(); + _events.clear(); + _events.addAll(keyDown.values); _processing = false; } - @override - Future onPointerDown(Vector2 localPosition, bool isMiddle) async { - if (!isMiddle) { - final action = _actions[InputType.LMB_DOWN]; - switch (action) { - case InputAction.PICK: - pickDelegate?.pick(localPosition); - default: - // noop - } - } - } - - @override - Future onPointerMove( - Vector2 localPosition, Vector2 delta, bool isMiddle) async { - if (_processing) { - return; - } - if (isMiddle) { - _inputDeltas[InputType.MMB_HOLD_AND_MOVE] = - (_inputDeltas[InputType.MMB_HOLD_AND_MOVE] ?? Vector3.zero()) + - Vector3(delta.x, delta.y, 0.0); - } else { - _inputDeltas[InputType.LMB_HOLD_AND_MOVE] = - (_inputDeltas[InputType.LMB_HOLD_AND_MOVE] ?? Vector3.zero()) + - Vector3(delta.x, delta.y, 0.0); - } - } - - @override - Future onPointerUp(bool isMiddle) async {} - - @override - Future onPointerHover(Vector2 localPosition, Vector2 delta) async { - if (_processing) { - return; - } - _inputDeltas[InputType.POINTER_MOVE] = - (_inputDeltas[InputType.POINTER_MOVE] ?? Vector3.zero()) + - Vector3(delta.x, delta.y, 0.0); - } - - @override - Future onPointerScroll( - Vector2 localPosition, double scrollDelta) async { - if (_processing) { - return; - } - try { - _inputDeltas[InputType.SCROLLWHEEL] = - (_inputDeltas[InputType.SCROLLWHEEL] ?? Vector3.zero()) + - Vector3(0, 0, scrollDelta > 0 ? 1 : -1); - } catch (e) { - _logger.warning("Error during scroll accumulation: $e"); - } - } - @override Future dispose() async { FilamentApp.instance!.unregisterRequestFrameHook(process); } @override - Future get initialized => viewer.initialized; - - @override - void setActionForType(InputType gestureType, InputAction gestureAction) { - _actions[gestureType] = gestureAction; - } - - @override - InputAction? getActionForType(InputType gestureType) { - return _actions[gestureType]; - } - - void keyDown(PhysicalKey key) { - _pressedKeys.add(key); - } - - void keyUp(PhysicalKey key) { - _pressedKeys.remove(key); - } - - @override - Future onScaleEnd(int pointerCount, double velocity) async {} - - @override - Future onScaleStart(Vector2 localPosition, int pointerCount, - Duration? sourceTimestamp) async { - // noop - } - - - @override - Future onScaleUpdate( - Vector2 focalPoint, - Vector2 focalPointDelta, - double horizontalScale, - double verticalScale, - double scale, - int pointerCount, - double rotation, - Duration? sourceTimestamp) async { - if (pointerCount == 1) { - _inputDeltas[InputType.SCALE1] = - Vector3(focalPointDelta.x, focalPointDelta.y, 0); - } else if (pointerCount == 2) { - _inputDeltas[InputType.SCALE2] = Vector3(0, 0, scale); - } else { - throw UnimplementedError("Only pointerCount <= 2 supported"); + Future handle(InputEvent event) async { + if (_processing) { + return; } - } - @override - Stream get transformUpdated => cameraUpdated; + _events.add(event); + } } diff --git a/thermion_dart/lib/src/input/src/delegates.dart b/thermion_dart/lib/src/input/src/delegates.dart deleted file mode 100644 index 9098ea7e..00000000 --- a/thermion_dart/lib/src/input/src/delegates.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:vector_math/vector_math_64.dart'; - -import 'input_handler.dart'; - -abstract class InputHandlerDelegate { - Future queue(InputAction action, Vector3? delta); - Future execute(); -} - -abstract class VelocityDelegate { - Vector2? get velocity; - - void updateVelocity(Vector2 delta); - - void startDeceleration(); - - void stopDeceleration(); - - void dispose() { - stopDeceleration(); - } -} - -abstract class PickDelegate { - const PickDelegate(); - void pick(Vector2 location); - Future dispose(); -} diff --git a/thermion_dart/lib/src/input/src/implementations/custom_camera_delegate_v2.dart b/thermion_dart/lib/src/input/src/implementations/custom_camera_delegate_v2.dart new file mode 100644 index 00000000..b4e5b7d1 --- /dev/null +++ b/thermion_dart/lib/src/input/src/implementations/custom_camera_delegate_v2.dart @@ -0,0 +1,228 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'package:vector_math/vector_math_64.dart'; +import '../../../viewer/viewer.dart'; +import '../../input.dart'; + +class CustomInputHandlerDelegate implements InputHandlerDelegate { + final View view; + final ThermionAsset asset; + + final InputSensitivityOptions sensitivity; + final Vector3 targetPoint; + final double minZoomDistance; + final double maxZoomDistance; + final worldUp = Vector3(0, 1, 0); + + double _radius; + double _radiusScaleFactor = 1.0; + double _azimuth; // Angle around worldUp (Y-axis), in radians + double + _elevation; // Angle above the XZ plane (around local X-axis), in radians + + bool _isInitialized = false; + bool _isMouseDown = false; + + Vector2? _lastPointerPosition; + + CustomInputHandlerDelegate( + this.view, this.asset, { + this.sensitivity = const InputSensitivityOptions(), + Vector3? targetPoint, + this.minZoomDistance = 1.0, + this.maxZoomDistance = 100.0, + }) : targetPoint = targetPoint ?? Vector3.zero(), + _radius = + (minZoomDistance + maxZoomDistance) / 2, // Initial default radius + _azimuth = 0.0, + _elevation = math.pi / 4; // Initial default elevation (45 degrees) + + Future _initializeFromCamera(Camera activeCamera) async { + final currentModelMatrix = await activeCamera.getModelMatrix(); + final cameraPosition = currentModelMatrix.getTranslation(); + final directionToCamera = cameraPosition - targetPoint; + + _radius = directionToCamera.length; + _radius = _radius.clamp(minZoomDistance, maxZoomDistance); + + if (_radius < 0.001) { + _radius = minZoomDistance; + _azimuth = 0.0; + _elevation = math.pi / 4; + } else { + final dirToCameraNormalized = directionToCamera.normalized(); + // Elevation: angle with the XZ plane (plane perpendicular to worldUp) + // Assuming worldUp is (0,1,0), elevation is asin(y) + _elevation = math.asin(dirToCameraNormalized.dot(worldUp)); + + // Azimuth: angle in the XZ plane. + // Project dirToCameraNormalized onto the plane perpendicular to worldUp + Vector3 projectionOnPlane = + (dirToCameraNormalized - worldUp * math.sin(_elevation)).normalized(); + if (projectionOnPlane.length2 < 0.0001 && + worldUp.dot(Vector3(0, 0, 1)).abs() < 0.99) { + // looking straight up/down, pick a default reference for azimuth + projectionOnPlane = + Vector3(0, 0, 1); // if worldUp is Y, project onto XZ plane + } else if (projectionOnPlane.length2 < 0.0001) { + // if worldUp is Z, project onto XY plane + projectionOnPlane = Vector3(1, 0, 0); + } + + // Define a reference vector in the plane (e.g., world X-axis or Z-axis) + // Let's use world Z-axis as the 0-azimuth reference, if not aligned with worldUp + Vector3 referenceAzimuthVector = Vector3(0, 0, 1); + if (worldUp.dot(referenceAzimuthVector).abs() > 0.99) { + // If worldUp is Z, use X instead + referenceAzimuthVector = Vector3(1, 0, 0); + } + // Ensure referenceAzimuthVector is also in the plane + referenceAzimuthVector = (referenceAzimuthVector - + worldUp * referenceAzimuthVector.dot(worldUp)) + .normalized(); + + _azimuth = math.atan2( + projectionOnPlane.cross(referenceAzimuthVector).dot(worldUp), + projectionOnPlane.dot(referenceAzimuthVector)); + } + _elevation = _elevation.clamp( + -math.pi / 2 + 0.01, math.pi / 2 - 0.01); // Clamp elevation + _isInitialized = true; + } + + @override + Future handle(Set events) async { + final activeCamera = await view.getCamera(); + if (!_isInitialized) { + await _initializeFromCamera(activeCamera); + } + + double deltaAzimuth = 0; + double deltaElevation = 0; + double deltaRadius = 0; + + for (final event in events) { + switch (event) { + case ScrollEvent(delta: final scrollDelta): + deltaRadius += sensitivity.scrollWheelSensitivity * scrollDelta; + break; + + case MouseEvent( + type: final type, + button: final button, + localPosition: final localPosition, + // delta: final mouseDelta // Using localPosition to calculate delta from _lastPointerPosition + ): + switch (type) { + case MouseEventType.buttonDown: + if (button == MouseButton.left) { + // Typically left mouse button for orbit + _isMouseDown = true; + _lastPointerPosition = localPosition; + } + break; + case MouseEventType.buttonUp: + if (button == MouseButton.left) { + _isMouseDown = false; + _lastPointerPosition = null; + } + break; + case MouseEventType.move: + case MouseEventType + .hover: // Some systems might only send hover when no buttons pressed + if (_isMouseDown && _lastPointerPosition != null) { + final dragDelta = localPosition - _lastPointerPosition!; + // X-drag affects azimuth, Y-drag affects elevation + deltaAzimuth -= dragDelta.x * + sensitivity.mouseSensitivity; // Invert X for natural feel + deltaElevation -= dragDelta.y * + sensitivity.mouseSensitivity; // Invert Y for natural feel + _lastPointerPosition = localPosition; + } else if (type == MouseEventType.hover) { + // Allow hover to set initial if not dragging + _lastPointerPosition = localPosition; + } + break; + } + break; + case TouchEvent( + type: final type, + localPosition: final localPosition, + delta: final touchDelta, + ): + switch (type) { + case TouchEventType.tap: + break; + default: + break; + } + break; + + case ScaleUpdateEvent( + numPointers: final numPointers, + scale: final scaleFactor, + localFocalPoint: final localFocalPoint, + localFocalPointDelta: final localFocalPointDelta + ): + if (numPointers == 1) { + deltaAzimuth -= + localFocalPointDelta!.$1 * sensitivity.touchSensitivity; + deltaElevation -= + localFocalPointDelta.$2 * sensitivity.touchSensitivity; + } else { + _radiusScaleFactor = scaleFactor; + } + case ScaleEndEvent(): + _radius *= _radiusScaleFactor; + _radiusScaleFactor = 1.0; + default: + break; + } + } + + if (deltaAzimuth == 0 && + deltaElevation == 0 && + deltaRadius == 0 && + _radiusScaleFactor == 1.0) { + return; + } + + _azimuth += deltaAzimuth; + _elevation += deltaElevation; + _radius += deltaRadius; + + var radius = _radius * _radiusScaleFactor; + + // Clamp parameters + _elevation = _elevation.clamp(-math.pi / 2 + 0.01, + math.pi / 2 - 0.01); // Prevent gimbal lock at poles + radius = radius.clamp(minZoomDistance, maxZoomDistance); + _azimuth = + _azimuth % (2 * math.pi); // Keep azimuth within 0-2PI range (optional) + + final double xOffset = radius * math.cos(_elevation) * math.sin(_azimuth); + final double yOffset = radius * math.sin(_elevation); + final double zOffset = radius * math.cos(_elevation) * math.cos(_azimuth); + + Vector3 cameraPosition; + if (worldUp.dot(Vector3(0, 1, 0)).abs() > 0.99) { + // Standard Y-up + cameraPosition = targetPoint + Vector3(xOffset, yOffset, zOffset); + } else if (worldUp.dot(Vector3(0, 0, 1)).abs() > 0.99) { + cameraPosition = targetPoint + + Vector3( + radius * math.cos(_elevation) * math.cos(_azimuth), // x + radius * math.cos(_elevation) * math.sin(_azimuth), // y + radius * math.sin(_elevation) // z + ); + } else { + cameraPosition = targetPoint + Vector3(xOffset, yOffset, zOffset); + } + + final modelMatrix = makeViewMatrix(cameraPosition, targetPoint, worldUp) + ..invert(); + + await activeCamera.setModelMatrix(modelMatrix); + } +} + diff --git a/thermion_dart/lib/src/input/src/implementations/default_pick_delegate.dart b/thermion_dart/lib/src/input/src/implementations/default_pick_delegate.dart index 5e73d754..cb8fe0b7 100644 --- a/thermion_dart/lib/src/input/src/implementations/default_pick_delegate.dart +++ b/thermion_dart/lib/src/input/src/implementations/default_pick_delegate.dart @@ -1,24 +1,24 @@ -import 'dart:async'; +// import 'dart:async'; -import 'package:thermion_dart/thermion_dart.dart'; -import 'package:vector_math/vector_math_64.dart'; +// import 'package:thermion_dart/thermion_dart.dart'; +// import 'package:vector_math/vector_math_64.dart'; -class DefaultPickDelegate extends PickDelegate { - final ThermionViewer viewer; +// class DefaultPickDelegate extends PickDelegate { +// final ThermionViewer viewer; - DefaultPickDelegate(this.viewer); +// DefaultPickDelegate(this.viewer); - final _picked = StreamController(); - Stream get picked => _picked.stream; +// final _picked = StreamController(); +// Stream get picked => _picked.stream; - Future dispose() async { - _picked.close(); - } +// Future dispose() async { +// _picked.close(); +// } - @override - void pick(Vector2 location) { - viewer.view.pick(location.x.toInt(), location.y.toInt(), (result) { - _picked.sink.add(result.entity); - }); - } -} +// @override +// void pick(Vector2 location) { +// viewer.view.pick(location.x.toInt(), location.y.toInt(), (result) { +// _picked.sink.add(result.entity); +// }); +// } +// } diff --git a/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_delegate_v2.dart b/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_delegate_v2.dart new file mode 100644 index 00000000..ea1ed352 --- /dev/null +++ b/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_delegate_v2.dart @@ -0,0 +1,235 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'package:vector_math/vector_math_64.dart'; +import '../../../viewer/viewer.dart'; +import '../../input.dart'; + +class OrbitInputHandlerDelegate implements InputHandlerDelegate { + final View view; + final InputSensitivityOptions sensitivity; + final Vector3 targetPoint; + final double minZoomDistance; + final double maxZoomDistance; + final worldUp = Vector3(0, 1, 0); + + double _radius; + double _radiusScaleFactor = 1.0; + double _azimuth; // Angle around worldUp (Y-axis), in radians + double + _elevation; // Angle above the XZ plane (around local X-axis), in radians + + bool _isInitialized = false; + bool _isMouseDown = false; + + Vector2? _lastPointerPosition; + + OrbitInputHandlerDelegate( + this.view, { + this.sensitivity = const InputSensitivityOptions(), + Vector3? targetPoint, + this.minZoomDistance = 1.0, + this.maxZoomDistance = 100.0, + }) : targetPoint = targetPoint ?? Vector3.zero(), + _radius = + (minZoomDistance + maxZoomDistance) / 2, // Initial default radius + _azimuth = 0.0, + _elevation = math.pi / 4; // Initial default elevation (45 degrees) + + Future _initializeFromCamera(Camera activeCamera) async { + final currentModelMatrix = await activeCamera.getModelMatrix(); + final cameraPosition = currentModelMatrix.getTranslation(); + final directionToCamera = cameraPosition - targetPoint; + + _radius = directionToCamera.length; + _radius = _radius.clamp(minZoomDistance, maxZoomDistance); + + if (_radius < 0.001) { + _radius = minZoomDistance; + _azimuth = 0.0; + _elevation = math.pi / 4; + } else { + final dirToCameraNormalized = directionToCamera.normalized(); + // Elevation: angle with the XZ plane (plane perpendicular to worldUp) + // Assuming worldUp is (0,1,0), elevation is asin(y) + _elevation = math.asin(dirToCameraNormalized.dot(worldUp)); + + // Azimuth: angle in the XZ plane. + // Project dirToCameraNormalized onto the plane perpendicular to worldUp + Vector3 projectionOnPlane = + (dirToCameraNormalized - worldUp * math.sin(_elevation)).normalized(); + if (projectionOnPlane.length2 < 0.0001 && + worldUp.dot(Vector3(0, 0, 1)).abs() < 0.99) { + // looking straight up/down, pick a default reference for azimuth + projectionOnPlane = + Vector3(0, 0, 1); // if worldUp is Y, project onto XZ plane + } else if (projectionOnPlane.length2 < 0.0001) { + // if worldUp is Z, project onto XY plane + projectionOnPlane = Vector3(1, 0, 0); + } + + // Define a reference vector in the plane (e.g., world X-axis or Z-axis) + // Let's use world Z-axis as the 0-azimuth reference, if not aligned with worldUp + Vector3 referenceAzimuthVector = Vector3(0, 0, 1); + if (worldUp.dot(referenceAzimuthVector).abs() > 0.99) { + // If worldUp is Z, use X instead + referenceAzimuthVector = Vector3(1, 0, 0); + } + // Ensure referenceAzimuthVector is also in the plane + referenceAzimuthVector = (referenceAzimuthVector - + worldUp * referenceAzimuthVector.dot(worldUp)) + .normalized(); + + _azimuth = math.atan2( + projectionOnPlane.cross(referenceAzimuthVector).dot(worldUp), + projectionOnPlane.dot(referenceAzimuthVector)); + } + _elevation = _elevation.clamp( + -math.pi / 2 + 0.01, math.pi / 2 - 0.01); // Clamp elevation + _isInitialized = true; + } + + @override + Future handle(Set events) async { + final activeCamera = await view.getCamera(); + if (!_isInitialized) { + await _initializeFromCamera(activeCamera); + } + + double deltaAzimuth = 0; + double deltaElevation = 0; + double deltaRadius = 0; + + for (final event in events) { + switch (event) { + case ScrollEvent(delta: final scrollDelta): + deltaRadius += sensitivity.scrollWheelSensitivity * scrollDelta; + break; + + case MouseEvent( + type: final type, + button: final button, + localPosition: final localPosition, + // delta: final mouseDelta // Using localPosition to calculate delta from _lastPointerPosition + ): + switch (type) { + case MouseEventType.buttonDown: + if (button == MouseButton.left) { + // Typically left mouse button for orbit + _isMouseDown = true; + _lastPointerPosition = localPosition; + } + break; + case MouseEventType.buttonUp: + if (button == MouseButton.left) { + _isMouseDown = false; + _lastPointerPosition = null; + } + break; + case MouseEventType.move: + case MouseEventType + .hover: // Some systems might only send hover when no buttons pressed + if (_isMouseDown && _lastPointerPosition != null) { + final dragDelta = localPosition - _lastPointerPosition!; + // X-drag affects azimuth, Y-drag affects elevation + deltaAzimuth -= dragDelta.x * + sensitivity.mouseSensitivity; // Invert X for natural feel + deltaElevation -= dragDelta.y * + sensitivity.mouseSensitivity; // Invert Y for natural feel + _lastPointerPosition = localPosition; + } else if (type == MouseEventType.hover) { + // Allow hover to set initial if not dragging + _lastPointerPosition = localPosition; + } + break; + } + break; + case TouchEvent( + type: final type, + localPosition: final localPosition, + delta: final touchDelta, + ): + switch (type) { + case TouchEventType.tap: + break; + default: + break; + } + break; + + case ScaleUpdateEvent( + numPointers: final numPointers, + scale: final scaleFactor, + localFocalPoint: final localFocalPoint, + localFocalPointDelta: final localFocalPointDelta + ): + if (numPointers == 1) { + deltaAzimuth -= localFocalPointDelta!.$1 * sensitivity.touchSensitivity; + deltaElevation -= localFocalPointDelta.$2 * sensitivity.touchSensitivity; + } else { + _radiusScaleFactor = scaleFactor; + } + case ScaleEndEvent(): + _radius *= _radiusScaleFactor; + _radiusScaleFactor = 1.0; + default: + break; + } + } + + if (deltaAzimuth == 0 && + deltaElevation == 0 && + deltaRadius == 0 && + _radiusScaleFactor == 1.0) { + return; + } + + _azimuth += deltaAzimuth; + _elevation += deltaElevation; + _radius += deltaRadius; + + var radius = _radius * _radiusScaleFactor; + + // Clamp parameters + _elevation = _elevation.clamp(-math.pi / 2 + 0.01, + math.pi / 2 - 0.01); // Prevent gimbal lock at poles + radius = radius.clamp(minZoomDistance, maxZoomDistance); + _azimuth = + _azimuth % (2 * math.pi); // Keep azimuth within 0-2PI range (optional) + + final double xOffset = radius * math.cos(_elevation) * math.sin(_azimuth); + final double yOffset = radius * math.sin(_elevation); + final double zOffset = radius * math.cos(_elevation) * math.cos(_azimuth); + + Vector3 cameraPosition; + if (worldUp.dot(Vector3(0, 1, 0)).abs() > 0.99) { + // Standard Y-up + cameraPosition = targetPoint + Vector3(xOffset, yOffset, zOffset); + } else if (worldUp.dot(Vector3(0, 0, 1)).abs() > 0.99) { + cameraPosition = targetPoint + + Vector3( + radius * math.cos(_elevation) * math.cos(_azimuth), // x + radius * math.cos(_elevation) * math.sin(_azimuth), // y + radius * math.sin(_elevation) // z + ); + } else { + cameraPosition = targetPoint + Vector3(xOffset, yOffset, zOffset); + } + + final modelMatrix = makeViewMatrix(cameraPosition, targetPoint, worldUp) + ..invert(); + + await activeCamera.setModelMatrix(modelMatrix); + } +} + + + // _lastPointerPosition = + // localFocalPoint; + // } else if (_isPointerDown && _lastPointerPosition != null) { + // final currentDragDelta = localPosition! - _lastPointerPosition!; + // deltaAzimuth -= + // currentDragDelta.x * sensitivity.touchSensitivity; + // deltaElevation -= + // currentDragDelta.y * sensitivity.touchSensitivity; + // _lastPointerPosition = localPosition; + // } \ No newline at end of file diff --git a/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_rotation_delegate.dart b/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_rotation_delegate.dart index d7508555..9b579248 100644 --- a/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_rotation_delegate.dart +++ b/thermion_dart/lib/src/input/src/implementations/fixed_orbit_camera_rotation_delegate.dart @@ -1,142 +1,143 @@ -import 'dart:async'; -import 'package:vector_math/vector_math_64.dart'; +// import 'dart:async'; +// import 'package:vector_math/vector_math_64.dart'; -import '../../../viewer/viewer.dart'; -import '../../input.dart'; +// import '../../../viewer/viewer.dart'; +// import '../../input.dart'; -/// -/// An [InputHandlerDelegate] that orbits the camera around a fixed -/// point. -/// -class FixedOrbitRotateInputHandlerDelegate implements InputHandlerDelegate { - final View view; - final double minimumDistance; - late final Vector3 target; +// /// +// /// An [InputHandlerDelegate] that orbits the camera around a fixed +// /// point. +// /// +// class FixedOrbitRotateInputHandlerDelegate implements InputHandlerDelegate { +// final View view; +// final double minimumDistance; +// late final Vector3 target; - final double rotationSensitivity; - final double zoomSensitivity; +// final double rotationSensitivity; +// final double zoomSensitivity; - Vector2 _queuedRotationDelta = Vector2.zero(); - double _queuedZoomDelta = 0.0; +// Vector2 _queuedRotationDelta = Vector2.zero(); +// double _queuedZoomDelta = 0.0; - Timer? _updateTimer; +// Timer? _updateTimer; - FixedOrbitRotateInputHandlerDelegate( - this.view, { - Vector3? target, - this.minimumDistance = 10.0, - this.rotationSensitivity = 0.01, - this.zoomSensitivity = 0.1, - }) { - this.target = target ?? Vector3.zero(); +// FixedOrbitRotateInputHandlerDelegate( +// this.view, { +// Vector3? target, +// this.minimumDistance = 10.0, +// this.rotationSensitivity = 0.01, +// this.zoomSensitivity = 0.1, +// }) { +// this.target = target ?? Vector3.zero(); - view.getCamera().then((camera) { - camera.lookAt(Vector3(0.0, 0, -minimumDistance), - focus: this.target, up: Vector3(0.0, 1.0, 0.0)); - }); - } +// view.getCamera().then((camera) { +// camera.lookAt(Vector3(0.0, 0, -minimumDistance), +// focus: this.target, up: Vector3(0.0, 1.0, 0.0)); +// }); +// } - void dispose() { - _updateTimer?.cancel(); - } +// void dispose() { +// _updateTimer?.cancel(); +// } - @override - Future queue(InputAction action, Vector3? delta) async { - if (delta == null) return; +// @override +// Future queue(InputAction action, Vector3? delta) async { +// if (delta == null) return; - switch (action) { - case InputAction.ROTATE: - _queuedRotationDelta += Vector2(delta.x, delta.y); - break; - case InputAction.TRANSLATE: - _queuedZoomDelta += delta.z; - break; - case InputAction.PICK: - break; - case InputAction.NONE: - // Do nothing - break; - case InputAction.ZOOM: - _queuedZoomDelta += delta.z; - break; - } - } +// switch (action) { +// case InputAction.ROTATE: +// _queuedRotationDelta += Vector2(delta.x, delta.y); +// break; +// case InputAction.TRANSLATE: +// _queuedZoomDelta += delta.z; +// break; +// case InputAction.PICK: +// break; +// case InputAction.NONE: +// // Do nothing +// break; +// case InputAction.ZOOM: +// _queuedZoomDelta -= (delta.z - 1.0); +// break; +// } +// } - bool _executing = false; +// bool _executing = false; - @override - Future execute() async { - if (_queuedRotationDelta.length2 == 0.0 && _queuedZoomDelta == 0.0) { - return null; - } +// @override +// Future execute() async { +// if (_queuedRotationDelta.length2 == 0.0 && _queuedZoomDelta == 0.0) { +// return null; +// } - if (_executing) { - return null; - } +// if (_executing) { +// return null; +// } - _executing = true; +// _executing = true; - final camera = await view.getCamera(); +// final camera = await view.getCamera(); - var modelMatrix = await camera.getModelMatrix(); - - Vector3 currentPosition = modelMatrix.getTranslation(); +// var modelMatrix = await camera.getModelMatrix(); - Vector3 forward = modelMatrix.forward; +// Vector3 currentPosition = modelMatrix.getTranslation(); - if (forward.length == 0) { - forward = Vector3(0, 0, -1); - currentPosition = Vector3(0, 0, minimumDistance); - } +// Vector3 forward = modelMatrix.forward; - Matrix4? updatedModelMatrix = null; +// if (forward.length == 0) { +// forward = Vector3(0, 0, -1); +// currentPosition = Vector3(0, 0, minimumDistance); +// } - // Zoom - if (_queuedZoomDelta != 0.0) { - var newPosition = currentPosition + - (currentPosition - target).scaled(_queuedZoomDelta * zoomSensitivity); +// Matrix4? updatedModelMatrix = null; - var distToTarget = (newPosition - target).length; +// // Zoom +// if (_queuedZoomDelta != 0.0) { +// print("_queuedZoomDelta $_queuedZoomDelta"); +// var newPosition = currentPosition + +// (currentPosition - target).scaled(_queuedZoomDelta * zoomSensitivity); - // if we somehow overshot the minimum distance, reset the camera to the minimum distance - if (distToTarget >= minimumDistance) { - currentPosition = newPosition; - // Calculate view matrix - forward = (currentPosition - target).normalized(); - var right = modelMatrix.up.cross(forward).normalized(); - var up = forward.cross(right); +// var distToTarget = (newPosition - target).length; - Matrix4 newViewMatrix = makeViewMatrix(currentPosition, target, up); - newViewMatrix.invert(); +// // if we somehow overshot the minimum distance, reset the camera to the minimum distance +// if (distToTarget >= minimumDistance) { +// currentPosition = newPosition; +// // Calculate view matrix +// forward = (currentPosition - target).normalized(); +// var right = modelMatrix.up.cross(forward).normalized(); +// var up = forward.cross(right); - await camera.setModelMatrix(newViewMatrix); - updatedModelMatrix = newViewMatrix; - } - } else if (_queuedRotationDelta.length != 0) { - double rotateX = _queuedRotationDelta.x * rotationSensitivity; - double rotateY = _queuedRotationDelta.y * rotationSensitivity; +// Matrix4 newViewMatrix = makeViewMatrix(currentPosition, target, up); +// newViewMatrix.invert(); - var modelMatrix = await camera.getModelMatrix(); +// await camera.setModelMatrix(newViewMatrix); +// updatedModelMatrix = newViewMatrix; +// } +// } else if (_queuedRotationDelta.length != 0) { +// double rotateX = _queuedRotationDelta.x * rotationSensitivity; +// double rotateY = _queuedRotationDelta.y * rotationSensitivity; - // for simplicity, we always assume a fixed coordinate system where - // we are rotating around world Y and camera X - var rot1 = Matrix4.identity() - ..setRotation(Quaternion.axisAngle(Vector3(0, 1, 0), -rotateX) - .asRotationMatrix()); - var rot2 = Matrix4.identity() - ..setRotation(Quaternion.axisAngle(modelMatrix.right, rotateY) - .asRotationMatrix()); +// var modelMatrix = await camera.getModelMatrix(); - modelMatrix = rot1 * rot2 * modelMatrix; - await camera.setModelMatrix(modelMatrix); - updatedModelMatrix = modelMatrix; - } +// // for simplicity, we always assume a fixed coordinate system where +// // we are rotating around world Y and camera X +// var rot1 = Matrix4.identity() +// ..setRotation(Quaternion.axisAngle(Vector3(0, 1, 0), -rotateX) +// .asRotationMatrix()); +// var rot2 = Matrix4.identity() +// ..setRotation(Quaternion.axisAngle(modelMatrix.right, rotateY) +// .asRotationMatrix()); - // Reset queued deltas - _queuedRotationDelta = Vector2.zero(); - _queuedZoomDelta = 0.0; +// modelMatrix = rot1 * rot2 * modelMatrix; +// await camera.setModelMatrix(modelMatrix); +// updatedModelMatrix = modelMatrix; +// } - _executing = false; - return updatedModelMatrix; - } -} +// // Reset queued deltas +// _queuedRotationDelta = Vector2.zero(); +// _queuedZoomDelta = 0.0; + +// _executing = false; +// return updatedModelMatrix; +// } +// } diff --git a/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate.dart b/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate.dart index 8fa3852d..f95f0609 100644 --- a/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate.dart +++ b/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate.dart @@ -1,133 +1,133 @@ -import 'dart:async'; -import 'package:vector_math/vector_math_64.dart'; -import '../../../viewer/viewer.dart'; -import '../delegates.dart'; -import '../input_handler.dart'; +// import 'dart:async'; +// import 'package:vector_math/vector_math_64.dart'; +// import '../../../viewer/viewer.dart'; +// import '../../input.dart'; -class FreeFlightInputHandlerDelegate implements InputHandlerDelegate { - final View view; +// class FreeFlightInputHandlerDelegate implements InputHandlerDelegate { +// final View view; - final Vector3? minBounds; - final Vector3? maxBounds; - final double rotationSensitivity; - final double movementSensitivity; - final double zoomSensitivity; - final double panSensitivity; - final double? clampY; +// final Vector3? minBounds; +// final Vector3? maxBounds; +// final double rotationSensitivity; +// final double movementSensitivity; +// final double zoomSensitivity; +// final double panSensitivity; +// final double? clampY; - Vector2 _queuedRotationDelta = Vector2.zero(); - Vector3 _queuedTranslateDelta = Vector3.zero(); - double _queuedZoomDelta = 0.0; - Vector3 _queuedMoveDelta = Vector3.zero(); +// Vector2 _queuedRotationDelta = Vector2.zero(); +// Vector3 _queuedTranslateDelta = Vector3.zero(); +// double _queuedZoomDelta = 0.0; +// Vector3 _queuedMoveDelta = Vector3.zero(); - FreeFlightInputHandlerDelegate(this.view, - {this.minBounds, - this.maxBounds, - this.rotationSensitivity = 0.001, - this.movementSensitivity = 0.1, - this.zoomSensitivity = 0.1, - this.panSensitivity = 0.1, - this.clampY}) {} +// FreeFlightInputHandlerDelegate(this.view, +// {this.minBounds, +// this.maxBounds, +// this.rotationSensitivity = 0.001, +// this.movementSensitivity = 0.1, +// this.zoomSensitivity = 0.1, +// this.panSensitivity = 0.1, +// this.clampY}) {} - @override - Future queue(InputAction action, Vector3? delta) async { - if (delta == null) return; +// @override +// Future queue(InputAction action, Vector3? delta) async { +// if (delta == null) return; - switch (action) { - case InputAction.ROTATE: - _queuedRotationDelta += Vector2(delta.x, delta.y); - break; - case InputAction.TRANSLATE: - _queuedTranslateDelta += delta; - break; - case InputAction.PICK: - _queuedZoomDelta += delta.z; - break; - case InputAction.NONE: - break; - case InputAction.ZOOM: - _queuedZoomDelta += delta.z; - break; - } - } +// switch (action) { +// case InputAction.ROTATE: +// _queuedRotationDelta += Vector2(delta.x, delta.y); +// break; +// case InputAction.TRANSLATE: +// _queuedTranslateDelta += delta; +// break; +// case InputAction.PICK: +// _queuedZoomDelta += delta.z; +// break; +// case InputAction.NONE: +// break; +// case InputAction.ZOOM: +// _queuedZoomDelta += delta.z; +// break; +// } +// } - bool _executing = false; +// bool _executing = false; - @override - Future execute() async { - if (_executing) { - return null; - } +// @override +// Future execute() async { +// if (_executing) { +// return null; +// } - _executing = true; +// _executing = true; - if (_queuedRotationDelta.length2 == 0.0 && - _queuedTranslateDelta.length2 == 0.0 && - _queuedZoomDelta == 0.0 && - _queuedMoveDelta.length2 == 0.0) { - _executing = false; - return null; - } +// if (_queuedRotationDelta.length2 == 0.0 && +// _queuedTranslateDelta.length2 == 0.0 && +// _queuedZoomDelta == 0.0 && +// _queuedMoveDelta.length2 == 0.0) { +// _executing = false; +// return null; +// } - final activeCamera = await view.getCamera(); +// final activeCamera = await view.getCamera(); - Matrix4 current = await activeCamera.getModelMatrix(); +// Matrix4 current = await activeCamera.getModelMatrix(); - Vector3 relativeTranslation = Vector3.zero(); - Quaternion relativeRotation = Quaternion.identity(); +// Vector3 relativeTranslation = Vector3.zero(); +// Quaternion relativeRotation = Quaternion.identity(); - if (_queuedRotationDelta.length2 > 0.0) { - double deltaX = _queuedRotationDelta.x * rotationSensitivity; - double deltaY = _queuedRotationDelta.y * rotationSensitivity; - relativeRotation = Quaternion.axisAngle(current.up, -deltaX) * - Quaternion.axisAngle(current.right, -deltaY); - _queuedRotationDelta = Vector2.zero(); - } +// if (_queuedRotationDelta.length2 > 0.0) { +// double deltaX = _queuedRotationDelta.x * rotationSensitivity; +// double deltaY = _queuedRotationDelta.y * rotationSensitivity; +// relativeRotation = Quaternion.axisAngle(current.up, -deltaX) * +// Quaternion.axisAngle(current.right, -deltaY); +// _queuedRotationDelta = Vector2.zero(); +// } - // Apply (mouse) pan - if (_queuedTranslateDelta.length2 > 0.0) { - double deltaX = -_queuedTranslateDelta.x * panSensitivity; - double deltaY = _queuedTranslateDelta.y * panSensitivity; - double deltaZ = -_queuedTranslateDelta.z * panSensitivity; +// // Apply (mouse) pan +// if (_queuedTranslateDelta.length2 > 0.0) { +// double deltaX = -_queuedTranslateDelta.x * panSensitivity; +// double deltaY = _queuedTranslateDelta.y * panSensitivity; +// double deltaZ = -_queuedTranslateDelta.z * panSensitivity; - relativeTranslation += current.right * deltaX + - current.up * deltaY + - current.forward * deltaZ; - _queuedTranslateDelta = Vector3.zero(); - } +// relativeTranslation += current.right * deltaX + +// current.up * deltaY + +// current.forward * deltaZ; +// _queuedTranslateDelta = Vector3.zero(); +// } - // Apply zoom - if (_queuedZoomDelta != 0.0) { - var zoomTranslation = current.forward..scaled(zoomSensitivity); - zoomTranslation.scale(_queuedZoomDelta); - relativeTranslation += zoomTranslation; - _queuedZoomDelta = 0.0; - } +// // Apply zoom +// if (_queuedZoomDelta != 0.0) { +// print("_queuedZoomDelta $_queuedZoomDelta"); +// var zoomTranslation = current.forward..scaled(zoomSensitivity); +// zoomTranslation.scale(_queuedZoomDelta); +// relativeTranslation += zoomTranslation; +// _queuedZoomDelta = 0.0; +// } - // Apply queued movement - if (_queuedMoveDelta.length2 > 0.0) { - relativeTranslation += (current.right * _queuedMoveDelta.x + - current.up * _queuedMoveDelta.y + - current.forward * _queuedMoveDelta.z) * - movementSensitivity; +// // Apply queued movement +// if (_queuedMoveDelta.length2 > 0.0) { +// relativeTranslation += (current.right * _queuedMoveDelta.x + +// current.up * _queuedMoveDelta.y + +// current.forward * _queuedMoveDelta.z) * +// movementSensitivity; - _queuedMoveDelta = Vector3.zero(); - } +// _queuedMoveDelta = Vector3.zero(); +// } - // // If the managed entity is not the active camera, we need to apply the rotation from the current camera model matrix - // // to the entity's translation - // if (await entity != activeCamera.getEntity()) { - // Matrix4 modelMatrix = await activeCamera.getModelMatrix(); - // relativeTranslation = modelMatrix.getRotation() * relativeTranslation; - // } +// // // If the managed entity is not the active camera, we need to apply the rotation from the current camera model matrix +// // // to the entity's translation +// // if (await entity != activeCamera.getEntity()) { +// // Matrix4 modelMatrix = await activeCamera.getModelMatrix(); +// // relativeTranslation = modelMatrix.getRotation() * relativeTranslation; +// // } - var updated = Matrix4.compose( - relativeTranslation, relativeRotation, Vector3(1, 1, 1)) * - current; - - await activeCamera.setModelMatrix(updated); +// var updated = Matrix4.compose( +// relativeTranslation, relativeRotation, Vector3(1, 1, 1)) * +// current; - _executing = false; - return updated; - } -} +// await activeCamera.setModelMatrix(updated); + +// _executing = false; +// return updated; +// } +// } diff --git a/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate_v2.dart b/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate_v2.dart new file mode 100644 index 00000000..34ebf734 --- /dev/null +++ b/thermion_dart/lib/src/input/src/implementations/free_flight_camera_delegate_v2.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +import 'package:vector_math/vector_math_64.dart'; +import '../../../viewer/viewer.dart'; +import '../../input.dart'; + +class FreeFlightInputHandlerDelegateV2 implements InputHandlerDelegate { + final View view; + + final InputSensitivityOptions sensitivity; + + FreeFlightInputHandlerDelegateV2(this.view, + {this.sensitivity = const InputSensitivityOptions()}); + + double? _scaleDelta; + @override + Future handle(Set events) async { + Vector2 rotation = Vector2.zero(); + Vector3 translation = Vector3.zero(); + + final activeCamera = await view.getCamera(); + + Matrix4 current = await activeCamera.getModelMatrix(); + + for (final event in events) { + switch (event) { + case ScrollEvent(delta: final delta): + translation += + Vector3(0, 0, sensitivity.scrollWheelSensitivity * delta); + case MouseEvent( + type: final type, + button: final button, + localPosition: final localPosition, + delta: final delta + ): + switch (type) { + case MouseEventType.hover: + case MouseEventType.move: + rotation += delta.scaled(sensitivity.mouseSensitivity); + default: + break; + } + break; + case TouchEvent(type: final type, delta: final delta): + switch (type) { + // case TouchEventType.move: + // rotation += delta!; + case TouchEventType.tap: + case TouchEventType.doubleTap: + break; + } + break; + case ScaleStartEvent(numPointers: final numPointers): + _scaleDelta = 1; + break; + case ScaleUpdateEvent( + numPointers: final numPointers, + localFocalPoint: final localFocalPoint, + localFocalPointDelta: final localFocalPointDelta, + scale: final scale, + ): + if (numPointers == 1) { + translation += + Vector3(localFocalPointDelta!.$1 * sensitivity.touchSensitivity, localFocalPointDelta!.$2 * sensitivity.touchSensitivity, 0); + } else { + translation = Vector3(0,0, (_scaleDelta! - scale) * sensitivity.touchScaleSensitivity * current.getTranslation().length.abs() ); + _scaleDelta = scale; + } + break; + case ScaleEndEvent(numPointers: final numPointers): + break; + case KeyEvent(type: final type, key: var key): + switch (key) { + case PhysicalKey.A: + translation += Vector3( + -sensitivity.keySensitivity, + 0, + 0, + ); + break; + case PhysicalKey.S: + translation += Vector3(0, 0, sensitivity.keySensitivity); + break; + case PhysicalKey.D: + translation += Vector3( + sensitivity.keySensitivity, + 0, + 0, + ); + break; + case PhysicalKey.W: + translation += Vector3( + 0, + 0, + -sensitivity.keySensitivity, + ); + break; + } + break; + } + } + + if (rotation.length2 + translation.length2 == 0.0) { + return; + } + + + + var updated = current * + Matrix4.compose( + translation, + Quaternion.axisAngle(Vector3(0, 1, 0), rotation.x) * + Quaternion.axisAngle(Vector3(1, 0, 0), rotation.y), + Vector3.all(1)); + + await activeCamera.setModelMatrix(updated); + + return updated; + } +} diff --git a/thermion_dart/lib/src/input/src/implementations/gizmo_input_handler.dart b/thermion_dart/lib/src/input/src/implementations/gizmo_input_handler.dart index abac3f19..6d47075d 100644 --- a/thermion_dart/lib/src/input/src/implementations/gizmo_input_handler.dart +++ b/thermion_dart/lib/src/input/src/implementations/gizmo_input_handler.dart @@ -1,371 +1,371 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:thermion_dart/thermion_dart.dart'; - -class _Gizmo { - final ThermionViewer viewer; - - final GizmoAsset _gizmo; - - final transformUpdates = StreamController<({Matrix4 transform})>.broadcast(); - - Axis? _active; - final GizmoType type; - - _Gizmo(this._gizmo, this.viewer, this.type); - - static Future<_Gizmo> forType(ThermionViewer viewer, GizmoType type) async { - final view = await viewer.view; - return _Gizmo(await viewer.getGizmo(type), viewer, type); - } - - Future dispose() async { - await transformUpdates.close(); - await viewer.destroyAsset(_gizmo); - } - - Future hide() async { - final scene = await viewer.view.getScene(); - await scene.remove(_gizmo); - } - - Future reveal() async { - final scene = await viewer.view.getScene(); - await scene.add(_gizmo); - gizmoTransform = await _gizmo.getWorldTransform(); - } - - double _getAngleBetweenVectors(Vector2 v1, Vector2 v2) { - // Normalize vectors to ensure consistent rotation regardless of distance from center - v1.normalize(); - v2.normalize(); - - // Calculate angle using atan2 - double angle = atan2(v2.y, v2.x) - atan2(v1.y, v1.x); - - // Ensure angle is between -π and π - if (angle > pi) angle -= 2 * pi; - if (angle < -pi) angle += 2 * pi; - - return angle; - } - - void checkHover(int x, int y) async { - _gizmo.pick(x, y, handler: (result, coords) async { - switch (result) { - case GizmoPickResultType.None: - await _gizmo.unhighlight(); - _active = null; - break; - case GizmoPickResultType.AxisX: - _active = Axis.X; - case GizmoPickResultType.AxisY: - _active = Axis.Y; - case GizmoPickResultType.AxisZ: - _active = Axis.Z; - default: - } - }); - } - - Matrix4? gizmoTransform; - - void _updateTransform(Vector2 currentPosition, Vector2 delta) async { - if (type == GizmoType.translation) { - await _updateTranslation(currentPosition, delta); - } else if (type == GizmoType.rotation) { - await _updateRotation(currentPosition, delta); - } - - await _gizmo.setTransform(gizmoTransform!); - - transformUpdates.add((transform: gizmoTransform!)); - } - - Future? _updateTranslation( - Vector2 currentPosition, Vector2 delta) async { - var view = await viewer.view; - var camera = await viewer.getActiveCamera(); - var viewport = await view.getViewport(); - var projectionMatrix = await camera.getProjectionMatrix(); - var viewMatrix = await camera.getViewMatrix(); - var inverseViewMatrix = await camera.getModelMatrix(); - var inverseProjectionMatrix = projectionMatrix.clone()..invert(); - - // get gizmo position in screenspace - var gizmoPositionWorldSpace = gizmoTransform!.getTranslation(); - Vector4 gizmoClipSpace = projectionMatrix * - viewMatrix * - Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y, - gizmoPositionWorldSpace.z, 1.0); - - var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w; - - var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width, - viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height)); - - gizmoScreenSpace += delta; - - gizmoNdc = Vector4(((gizmoScreenSpace.x / viewport.width) - 0.5) * 2, - (((gizmoScreenSpace.y / viewport.height)) - 0.5) * -2, gizmoNdc.z, 1.0); - - var gizmoViewSpace = inverseProjectionMatrix * gizmoNdc; - gizmoViewSpace /= gizmoViewSpace.w; - - var newPosition = (inverseViewMatrix * gizmoViewSpace).xyz; - - Vector3 worldSpaceDelta = newPosition - gizmoTransform!.getTranslation(); - worldSpaceDelta.multiply(_active!.asVector()); - - gizmoTransform! - .setTranslation(gizmoTransform!.getTranslation() + worldSpaceDelta); - } - - Future? _updateRotation(Vector2 currentPosition, Vector2 delta) async { - var camera = await viewer.view.getCamera(); - var viewport = await viewer.view.getViewport(); - var projectionMatrix = await camera.getProjectionMatrix(); - var viewMatrix = await camera.getViewMatrix(); - - // Get gizmo center in screen space - var gizmoPositionWorldSpace = gizmoTransform!.getTranslation(); - Vector4 gizmoClipSpace = projectionMatrix * - viewMatrix * - Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y, - gizmoPositionWorldSpace.z, 1.0); - - var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w; - var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width, - viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height)); - - // Calculate vectors from gizmo center to previous and current mouse positions - var prevVector = (currentPosition - delta) - gizmoScreenSpace; - var currentVector = currentPosition - gizmoScreenSpace; - - // Calculate rotation angle based on the active axis - double rotationAngle = 0.0; - switch (_active) { - case Axis.X: - // For X axis, project onto YZ plane - var prev = Vector2(prevVector.y, -prevVector.x); - var curr = Vector2(currentVector.y, -currentVector.x); - rotationAngle = _getAngleBetweenVectors(prev, curr); - break; - case Axis.Y: - // For Y axis, project onto XZ plane - var prev = Vector2(prevVector.x, -prevVector.y); - var curr = Vector2(currentVector.x, -currentVector.y); - rotationAngle = _getAngleBetweenVectors(prev, curr); - break; - case Axis.Z: - // For Z axis, use screen plane directly - rotationAngle = -1 * _getAngleBetweenVectors(prevVector, currentVector); - break; - default: - return; - } - - // Create rotation matrix based on the active axis - var rotationMatrix = Matrix4.identity(); - switch (_active) { - case Axis.X: - rotationMatrix.setRotationX(rotationAngle); - break; - case Axis.Y: - rotationMatrix.setRotationY(rotationAngle); - break; - case Axis.Z: - rotationMatrix.setRotationZ(rotationAngle); - break; - default: - return; - } - - // Apply rotation to the current transform - gizmoTransform = gizmoTransform! * rotationMatrix; - } -} - -class GizmoInputHandler extends InputHandler { - final ThermionViewer viewer; - - late final _gizmos = {}; - - _Gizmo? _active; - - ThermionEntity? _attached; - - Future attach(ThermionEntity entity) async { - if (_attached != null) { - await detach(); - } - _attached = entity; - if (_active != null) { - await FilamentApp.instance!.setParent(_attached!, _active!._gizmo.entity); - await _active!.reveal(); - } - } - - Future getGizmoTransform() async { - return _active?.gizmoTransform; - } - - Future detach() async { - if (_attached == null) { - return; - } - await FilamentApp.instance!.setParent(_attached!, null); - await _active?.hide(); - _attached = null; - } - - final _initialized = Completer(); - - final _transformController = StreamController.broadcast(); - Stream get transformUpdated => _transformController.stream; - - final _pickResultController = StreamController.broadcast(); - Stream get onPickResult => _pickResultController.stream; - - GizmoInputHandler({required this.viewer, required GizmoType initialType}) { - initialize().then((_) { - setGizmoType(initialType); - }); - } - - GizmoType? getGizmoType() { - return _active?.type; - } - - Future setGizmoType(GizmoType? type) async { - if (type == null) { - await detach(); - _active?.hide(); - _active = null; - } else { - _active?.hide(); - _active = _gizmos[type]!; - _active!.reveal(); - if (_attached != null) { - await attach(_attached!); - } - } - } - - Future initialize() async { - if (_initialized.isCompleted) { - throw Exception("Already initialized"); - } - await viewer.initialized; - - _gizmos[GizmoType.translation] = - await _Gizmo.forType(viewer, GizmoType.translation); - _gizmos[GizmoType.rotation] = - await _Gizmo.forType(viewer, GizmoType.rotation); - await setGizmoType(GizmoType.translation); - for (final gizmo in _gizmos.values) { - gizmo.transformUpdates.stream.listen((update) { - _transformController.add(update.transform); - }); - } - _initialized.complete(true); - } - - @override - Future dispose() async { - _gizmos[GizmoType.rotation]!.dispose(); - _gizmos[GizmoType.translation]!.dispose(); - _gizmos.clear(); - } - - @override - InputAction? getActionForType(InputType gestureType) { - if (gestureType == InputType.LMB_DOWN) { - return InputAction.PICK; - } - throw UnimplementedError(); - } - - @override - Future get initialized => _initialized.future; - - @override - void keyDown(PhysicalKey key) {} - - @override - void keyUp(PhysicalKey key) {} - - @override - Future? onPointerDown(Vector2 localPosition, bool isMiddle) async { - if (!_initialized.isCompleted) { - return; - } - - if (isMiddle) { - return; - } - - await viewer.view.pick(localPosition.x.toInt(), localPosition.y.toInt(), - (result) async { - if (_active?._gizmo.isNonPickable(result.entity) == true || - result.entity == FILAMENT_ENTITY_NULL) { - _pickResultController.add(null); - return; - } - if (_active?._gizmo.isGizmoEntity(result.entity) != true) { - _pickResultController.add(result.entity); - } - }); - } - - @override - Future? onPointerHover(Vector2 localPosition, Vector2 delta) async { - if (!_initialized.isCompleted) { - return; - } - _active?.checkHover(localPosition.x.floor(), localPosition.y.floor()); - } - - @override - Future? onPointerMove( - Vector2 localPosition, Vector2 delta, bool isMiddle) async { - if (!isMiddle && _active?._active != null) { - final scaledDelta = Vector2( - delta.x, - delta.y, - ); - _active!._updateTransform(localPosition, scaledDelta); - return; - } - } - - @override - Future? onPointerScroll( - Vector2 localPosition, double scrollDelta) async {} - - @override - Future? onPointerUp(bool isMiddle) async {} - - @override - Future? onScaleEnd(int pointerCount, double velocity) {} - - @override - Future? onScaleStart( - Vector2 focalPoint, int pointerCount, Duration? sourceTimestamp) {} - - @override - Future? onScaleUpdate( - Vector2 focalPoint, - Vector2 focalPointDelta, - double horizontalScale, - double verticalScale, - double scale, - int pointerCount, - double rotation, - Duration? sourceTimestamp) {} - - @override - void setActionForType(InputType gestureType, InputAction gestureAction) { - throw UnimplementedError(); - } -} +// import 'dart:async'; +// import 'dart:math'; +// import 'package:thermion_dart/thermion_dart.dart'; + +// class _Gizmo { +// final ThermionViewer viewer; + +// final GizmoAsset _gizmo; + +// final transformUpdates = StreamController<({Matrix4 transform})>.broadcast(); + +// Axis? _active; +// final GizmoType type; + +// _Gizmo(this._gizmo, this.viewer, this.type); + +// static Future<_Gizmo> forType(ThermionViewer viewer, GizmoType type) async { +// final view = await viewer.view; +// return _Gizmo(await viewer.getGizmo(type), viewer, type); +// } + +// Future dispose() async { +// await transformUpdates.close(); +// await viewer.destroyAsset(_gizmo); +// } + +// Future hide() async { +// final scene = await viewer.view.getScene(); +// await scene.remove(_gizmo); +// } + +// Future reveal() async { +// final scene = await viewer.view.getScene(); +// await scene.add(_gizmo); +// gizmoTransform = await _gizmo.getWorldTransform(); +// } + +// double _getAngleBetweenVectors(Vector2 v1, Vector2 v2) { +// // Normalize vectors to ensure consistent rotation regardless of distance from center +// v1.normalize(); +// v2.normalize(); + +// // Calculate angle using atan2 +// double angle = atan2(v2.y, v2.x) - atan2(v1.y, v1.x); + +// // Ensure angle is between -π and π +// if (angle > pi) angle -= 2 * pi; +// if (angle < -pi) angle += 2 * pi; + +// return angle; +// } + +// void checkHover(int x, int y) async { +// _gizmo.pick(x, y, handler: (result, coords) async { +// switch (result) { +// case GizmoPickResultType.None: +// await _gizmo.unhighlight(); +// _active = null; +// break; +// case GizmoPickResultType.AxisX: +// _active = Axis.X; +// case GizmoPickResultType.AxisY: +// _active = Axis.Y; +// case GizmoPickResultType.AxisZ: +// _active = Axis.Z; +// default: +// } +// }); +// } + +// Matrix4? gizmoTransform; + +// void _updateTransform(Vector2 currentPosition, Vector2 delta) async { +// if (type == GizmoType.translation) { +// await _updateTranslation(currentPosition, delta); +// } else if (type == GizmoType.rotation) { +// await _updateRotation(currentPosition, delta); +// } + +// await _gizmo.setTransform(gizmoTransform!); + +// transformUpdates.add((transform: gizmoTransform!)); +// } + +// Future? _updateTranslation( +// Vector2 currentPosition, Vector2 delta) async { +// var view = await viewer.view; +// var camera = await viewer.getActiveCamera(); +// var viewport = await view.getViewport(); +// var projectionMatrix = await camera.getProjectionMatrix(); +// var viewMatrix = await camera.getViewMatrix(); +// var inverseViewMatrix = await camera.getModelMatrix(); +// var inverseProjectionMatrix = projectionMatrix.clone()..invert(); + +// // get gizmo position in screenspace +// var gizmoPositionWorldSpace = gizmoTransform!.getTranslation(); +// Vector4 gizmoClipSpace = projectionMatrix * +// viewMatrix * +// Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y, +// gizmoPositionWorldSpace.z, 1.0); + +// var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w; + +// var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width, +// viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height)); + +// gizmoScreenSpace += delta; + +// gizmoNdc = Vector4(((gizmoScreenSpace.x / viewport.width) - 0.5) * 2, +// (((gizmoScreenSpace.y / viewport.height)) - 0.5) * -2, gizmoNdc.z, 1.0); + +// var gizmoViewSpace = inverseProjectionMatrix * gizmoNdc; +// gizmoViewSpace /= gizmoViewSpace.w; + +// var newPosition = (inverseViewMatrix * gizmoViewSpace).xyz; + +// Vector3 worldSpaceDelta = newPosition - gizmoTransform!.getTranslation(); +// worldSpaceDelta.multiply(_active!.asVector()); + +// gizmoTransform! +// .setTranslation(gizmoTransform!.getTranslation() + worldSpaceDelta); +// } + +// Future? _updateRotation(Vector2 currentPosition, Vector2 delta) async { +// var camera = await viewer.view.getCamera(); +// var viewport = await viewer.view.getViewport(); +// var projectionMatrix = await camera.getProjectionMatrix(); +// var viewMatrix = await camera.getViewMatrix(); + +// // Get gizmo center in screen space +// var gizmoPositionWorldSpace = gizmoTransform!.getTranslation(); +// Vector4 gizmoClipSpace = projectionMatrix * +// viewMatrix * +// Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y, +// gizmoPositionWorldSpace.z, 1.0); + +// var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w; +// var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width, +// viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height)); + +// // Calculate vectors from gizmo center to previous and current mouse positions +// var prevVector = (currentPosition - delta) - gizmoScreenSpace; +// var currentVector = currentPosition - gizmoScreenSpace; + +// // Calculate rotation angle based on the active axis +// double rotationAngle = 0.0; +// switch (_active) { +// case Axis.X: +// // For X axis, project onto YZ plane +// var prev = Vector2(prevVector.y, -prevVector.x); +// var curr = Vector2(currentVector.y, -currentVector.x); +// rotationAngle = _getAngleBetweenVectors(prev, curr); +// break; +// case Axis.Y: +// // For Y axis, project onto XZ plane +// var prev = Vector2(prevVector.x, -prevVector.y); +// var curr = Vector2(currentVector.x, -currentVector.y); +// rotationAngle = _getAngleBetweenVectors(prev, curr); +// break; +// case Axis.Z: +// // For Z axis, use screen plane directly +// rotationAngle = -1 * _getAngleBetweenVectors(prevVector, currentVector); +// break; +// default: +// return; +// } + +// // Create rotation matrix based on the active axis +// var rotationMatrix = Matrix4.identity(); +// switch (_active) { +// case Axis.X: +// rotationMatrix.setRotationX(rotationAngle); +// break; +// case Axis.Y: +// rotationMatrix.setRotationY(rotationAngle); +// break; +// case Axis.Z: +// rotationMatrix.setRotationZ(rotationAngle); +// break; +// default: +// return; +// } + +// // Apply rotation to the current transform +// gizmoTransform = gizmoTransform! * rotationMatrix; +// } +// } + +// class GizmoInputHandler extends InputHandler { +// final ThermionViewer viewer; + +// late final _gizmos = {}; + +// _Gizmo? _active; + +// ThermionEntity? _attached; + +// Future attach(ThermionEntity entity) async { +// if (_attached != null) { +// await detach(); +// } +// _attached = entity; +// if (_active != null) { +// await FilamentApp.instance!.setParent(_attached!, _active!._gizmo.entity); +// await _active!.reveal(); +// } +// } + +// Future getGizmoTransform() async { +// return _active?.gizmoTransform; +// } + +// Future detach() async { +// if (_attached == null) { +// return; +// } +// await FilamentApp.instance!.setParent(_attached!, null); +// await _active?.hide(); +// _attached = null; +// } + +// final _initialized = Completer(); + +// final _transformController = StreamController.broadcast(); +// Stream get transformUpdated => _transformController.stream; + +// final _pickResultController = StreamController.broadcast(); +// Stream get onPickResult => _pickResultController.stream; + +// GizmoInputHandler({required this.viewer, required GizmoType initialType}) { +// initialize().then((_) { +// setGizmoType(initialType); +// }); +// } + +// GizmoType? getGizmoType() { +// return _active?.type; +// } + +// Future setGizmoType(GizmoType? type) async { +// if (type == null) { +// await detach(); +// _active?.hide(); +// _active = null; +// } else { +// _active?.hide(); +// _active = _gizmos[type]!; +// _active!.reveal(); +// if (_attached != null) { +// await attach(_attached!); +// } +// } +// } + +// Future initialize() async { +// if (_initialized.isCompleted) { +// throw Exception("Already initialized"); +// } +// await viewer.initialized; + +// _gizmos[GizmoType.translation] = +// await _Gizmo.forType(viewer, GizmoType.translation); +// _gizmos[GizmoType.rotation] = +// await _Gizmo.forType(viewer, GizmoType.rotation); +// await setGizmoType(GizmoType.translation); +// for (final gizmo in _gizmos.values) { +// gizmo.transformUpdates.stream.listen((update) { +// _transformController.add(update.transform); +// }); +// } +// _initialized.complete(true); +// } + +// @override +// Future dispose() async { +// _gizmos[GizmoType.rotation]!.dispose(); +// _gizmos[GizmoType.translation]!.dispose(); +// _gizmos.clear(); +// } + +// @override +// InputAction? getActionForType(InputType gestureType) { +// if (gestureType == InputType.LMB_DOWN) { +// return InputAction.PICK; +// } +// throw UnimplementedError(); +// } + +// @override +// Future get initialized => _initialized.future; + +// @override +// void keyDown(PhysicalKey key) {} + +// @override +// void keyUp(PhysicalKey key) {} + +// @override +// Future? onPointerDown(Vector2 localPosition, bool isMiddle) async { +// if (!_initialized.isCompleted) { +// return; +// } + +// if (isMiddle) { +// return; +// } + +// await viewer.view.pick(localPosition.x.toInt(), localPosition.y.toInt(), +// (result) async { +// if (_active?._gizmo.isNonPickable(result.entity) == true || +// result.entity == FILAMENT_ENTITY_NULL) { +// _pickResultController.add(null); +// return; +// } +// if (_active?._gizmo.isGizmoEntity(result.entity) != true) { +// _pickResultController.add(result.entity); +// } +// }); +// } + +// @override +// Future? onPointerHover(Vector2 localPosition, Vector2 delta) async { +// if (!_initialized.isCompleted) { +// return; +// } +// _active?.checkHover(localPosition.x.floor(), localPosition.y.floor()); +// } + +// @override +// Future? onPointerMove( +// Vector2 localPosition, Vector2 delta, bool isMiddle) async { +// if (!isMiddle && _active?._active != null) { +// final scaledDelta = Vector2( +// delta.x, +// delta.y, +// ); +// _active!._updateTransform(localPosition, scaledDelta); +// return; +// } +// } + +// @override +// Future? onPointerScroll( +// Vector2 localPosition, double scrollDelta) async {} + +// @override +// Future? onPointerUp(bool isMiddle) async {} + +// @override +// Future? onScaleEnd(int pointerCount, double velocity) {} + +// @override +// Future? onScaleStart( +// Vector2 focalPoint, int pointerCount, Duration? sourceTimestamp) {} + +// @override +// Future? onScaleUpdate( +// Vector2 focalPoint, +// Vector2 focalPointDelta, +// double horizontalScale, +// double verticalScale, +// double scale, +// int pointerCount, +// double rotation, +// Duration? sourceTimestamp) {} + +// @override +// void setActionForType(InputType gestureType, InputAction gestureAction) { +// throw UnimplementedError(); +// } +// } diff --git a/thermion_dart/lib/src/input/src/implementations/third_person_camera_delegate.dart b/thermion_dart/lib/src/input/src/implementations/third_person_camera_delegate.dart index 212e43d9..855382d4 100644 --- a/thermion_dart/lib/src/input/src/implementations/third_person_camera_delegate.dart +++ b/thermion_dart/lib/src/input/src/implementations/third_person_camera_delegate.dart @@ -1,121 +1,120 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:vector_math/vector_math_64.dart'; -import '../../../viewer/viewer.dart'; -import '../delegates.dart'; -import '../input_handler.dart'; +// import 'dart:async'; +// import 'dart:math'; +// import 'package:vector_math/vector_math_64.dart'; +// import '../../../viewer/viewer.dart'; +// import '../../input.dart'; -class OverTheShoulderCameraDelegate implements InputHandlerDelegate { - final ThermionViewer viewer; +// class OverTheShoulderCameraDelegate implements InputHandlerDelegate { +// final ThermionViewer viewer; - late ThermionAsset player; - late Camera camera; +// late ThermionAsset player; +// late Camera camera; - final double rotationSensitivity; - final double movementSensitivity; - final double zoomSensitivity; - final double panSensitivity; - final double? clampY; +// final double rotationSensitivity; +// final double movementSensitivity; +// final double zoomSensitivity; +// final double panSensitivity; +// final double? clampY; - static final _up = Vector3(0, 1, 0); - static final _forward = Vector3(0, 0, -1); - static final Vector3 _right = Vector3(1, 0, 0); +// static final _up = Vector3(0, 1, 0); +// static final _forward = Vector3(0, 0, -1); +// static final Vector3 _right = Vector3(1, 0, 0); - Vector2 _queuedRotationDelta = Vector2.zero(); - double _queuedZoomDelta = 0.0; - Vector3 _queuedMoveDelta = Vector3.zero(); +// Vector2 _queuedRotationDelta = Vector2.zero(); +// double _queuedZoomDelta = 0.0; +// Vector3 _queuedMoveDelta = Vector3.zero(); - final cameraPosition = Vector3(-0.5, 2.5, -3); - final cameraUp = Vector3(0, 1, 0); - var cameraLookAt = Vector3(0, 0.5, 3); +// final cameraPosition = Vector3(-0.5, 2.5, -3); +// final cameraUp = Vector3(0, 1, 0); +// var cameraLookAt = Vector3(0, 0.5, 3); - final void Function(Matrix4 transform)? onUpdate; +// final void Function(Matrix4 transform)? onUpdate; - OverTheShoulderCameraDelegate(this.viewer, this.player, this.camera, - {this.rotationSensitivity = 0.001, - this.movementSensitivity = 0.1, - this.zoomSensitivity = 0.1, - this.panSensitivity = 0.1, - this.clampY, - ThermionEntity? entity, - this.onUpdate}) {} +// OverTheShoulderCameraDelegate(this.viewer, this.player, this.camera, +// {this.rotationSensitivity = 0.001, +// this.movementSensitivity = 0.1, +// this.zoomSensitivity = 0.1, +// this.panSensitivity = 0.1, +// this.clampY, +// ThermionEntity? entity, +// this.onUpdate}) {} - @override - Future queue(InputAction action, Vector3? delta) async { - if (delta == null) return; +// @override +// Future queue(InputAction action, Vector3? delta) async { +// if (delta == null) return; - switch (action) { - case InputAction.ROTATE: - _queuedRotationDelta += Vector2(delta.x, delta.y); - break; - case InputAction.TRANSLATE: - _queuedMoveDelta += delta; - break; - case InputAction.PICK: - _queuedZoomDelta += delta.z; - break; - case InputAction.NONE: - break; - case InputAction.ZOOM: - break; - } - } +// switch (action) { +// case InputAction.ROTATE: +// _queuedRotationDelta += Vector2(delta.x, delta.y); +// break; +// case InputAction.TRANSLATE: +// _queuedMoveDelta += delta; +// break; +// case InputAction.PICK: +// _queuedZoomDelta += delta.z; +// break; +// case InputAction.NONE: +// break; +// case InputAction.ZOOM: +// break; +// } +// } - static bool _executing = false; - static bool get executing => _executing; +// static bool _executing = false; +// static bool get executing => _executing; - @override - Future execute() async { - if (_executing) { - return null; - } +// @override +// Future execute() async { +// if (_executing) { +// return null; +// } - _executing = true; +// _executing = true; - if (_queuedRotationDelta.length2 == 0.0 && - _queuedZoomDelta == 0.0 && - _queuedMoveDelta.length2 == 0.0) { - _executing = false; - return null; - } +// if (_queuedRotationDelta.length2 == 0.0 && +// _queuedZoomDelta == 0.0 && +// _queuedMoveDelta.length2 == 0.0) { +// _executing = false; +// return null; +// } - Matrix4 currentPlayerTransform = await player.getWorldTransform(); +// Matrix4 currentPlayerTransform = await player.getWorldTransform(); - // first we need to convert the move vector to player space - var newTransform = - Matrix4.translation(_queuedMoveDelta * movementSensitivity); +// // first we need to convert the move vector to player space +// var newTransform = +// Matrix4.translation(_queuedMoveDelta * movementSensitivity); - _queuedMoveDelta = Vector3.zero(); - Matrix4 newPlayerTransform = newTransform * currentPlayerTransform; - await player.setTransform(newPlayerTransform); +// _queuedMoveDelta = Vector3.zero(); +// Matrix4 newPlayerTransform = newTransform * currentPlayerTransform; +// await player.setTransform(newPlayerTransform); - if (_queuedZoomDelta != 0.0) { - // Ignore zoom - } +// if (_queuedZoomDelta != 0.0) { +// // Ignore zoom +// } - var inverted = newPlayerTransform.clone()..invert(); +// var inverted = newPlayerTransform.clone()..invert(); - // camera is always looking at -Z, whereas models generally face towards +Z - if (_queuedRotationDelta.length2 > 0.0) { - double deltaX = _queuedRotationDelta.x * rotationSensitivity; - double deltaY = _queuedRotationDelta.y * rotationSensitivity; +// // camera is always looking at -Z, whereas models generally face towards +Z +// if (_queuedRotationDelta.length2 > 0.0) { +// double deltaX = _queuedRotationDelta.x * rotationSensitivity; +// double deltaY = _queuedRotationDelta.y * rotationSensitivity; - cameraLookAt = Matrix4.rotationY(-deltaX) * - Matrix4.rotationX(-deltaY) * - cameraLookAt; - _queuedRotationDelta = Vector2.zero(); - } +// cameraLookAt = Matrix4.rotationY(-deltaX) * +// Matrix4.rotationX(-deltaY) * +// cameraLookAt; +// _queuedRotationDelta = Vector2.zero(); +// } - var newCameraViewMatrix = - makeViewMatrix(cameraPosition, cameraLookAt, cameraUp); - newCameraViewMatrix.invert(); - var newCameraTransform = newPlayerTransform * newCameraViewMatrix; - await camera.setTransform(newCameraTransform); +// var newCameraViewMatrix = +// makeViewMatrix(cameraPosition, cameraLookAt, cameraUp); +// newCameraViewMatrix.invert(); +// var newCameraTransform = newPlayerTransform * newCameraViewMatrix; +// await camera.setTransform(newCameraTransform); - // await viewer.queueTransformUpdates( - // [camera.getEntity(), player], [newCameraTransform, newPlayerTransform]); - onUpdate?.call(newPlayerTransform); - _executing = false; - return newCameraTransform; - } -} +// // await viewer.queueTransformUpdates( +// // [camera.getEntity(), player], [newCameraTransform, newPlayerTransform]); +// onUpdate?.call(newPlayerTransform); +// _executing = false; +// return newCameraTransform; +// } +// } diff --git a/thermion_dart/lib/src/input/src/input_handler.dart b/thermion_dart/lib/src/input/src/input_handler.dart index b0b20af8..7c00cc34 100644 --- a/thermion_dart/lib/src/input/src/input_handler.dart +++ b/thermion_dart/lib/src/input/src/input_handler.dart @@ -1,64 +1,32 @@ import 'dart:async'; -import 'package:vector_math/vector_math_64.dart'; - -enum InputType { - LMB_DOWN, - LMB_HOLD_AND_MOVE, - LMB_UP, - LMB_HOVER, - MMB_DOWN, - MMB_HOLD_AND_MOVE, - - MMB_UP, - MMB_HOVER, - SCALE1, - SCALE2, // two fingers pinchin in/out - SCALE2_ROTATE, // two fingers rotating in a circle - SCALE2_MOVE, // two fingers sliding along a line - SCROLLWHEEL, - POINTER_MOVE, - KEYDOWN_W, - KEYDOWN_A, - KEYDOWN_S, - KEYDOWN_D, -} - -enum PhysicalKey { W, A, S, D } - -enum InputAction { TRANSLATE, ROTATE, PICK, ZOOM, NONE } +import 'input_types.dart'; +/// +/// An interface for handling user device input events. +/// abstract class InputHandler { - - @Deprecated("Use @transformUpdated instead") - Stream get cameraUpdated => transformUpdated; + /// + /// + /// + Future? handle(InputEvent event); - Stream get transformUpdated; - - Future? onPointerHover(Vector2 localPosition, Vector2 delta); - Future? onPointerScroll(Vector2 localPosition, double scrollDelta); - Future? onPointerDown(Vector2 localPosition, bool isMiddle); - Future? onPointerMove( - Vector2 localPosition, Vector2 delta, bool isMiddle); - Future? onPointerUp(bool isMiddle); - Future? onScaleStart( - Vector2 focalPoint, int pointerCount, Duration? sourceTimestamp); - Future? onScaleUpdate( - Vector2 focalPoint, - Vector2 focalPointDelta, - double horizontalScale, - double verticalScale, - double scale, - int pointerCount, - double rotation, - Duration? sourceTimestamp); - Future? onScaleEnd(int pointerCount, double velocity); - Future get initialized; + /// + /// + /// Future dispose(); - - void setActionForType(InputType gestureType, InputAction gestureAction); - InputAction? getActionForType(InputType gestureType); - - void keyDown(PhysicalKey key); - void keyUp(PhysicalKey key); - +} + +class InputSensitivityOptions { + final double touchSensitivity; + final double touchScaleSensitivity; + final double mouseSensitivity; + final double keySensitivity; + final double scrollWheelSensitivity; + + const InputSensitivityOptions( + {this.touchSensitivity = 0.001, + this.touchScaleSensitivity = 2.0, + this.mouseSensitivity = 0.001, + this.scrollWheelSensitivity = 0.01, + this.keySensitivity = 0.1}); } diff --git a/thermion_dart/lib/src/input/src/input_types.dart b/thermion_dart/lib/src/input/src/input_types.dart new file mode 100644 index 00000000..8f5b7a8a --- /dev/null +++ b/thermion_dart/lib/src/input/src/input_types.dart @@ -0,0 +1,92 @@ +import 'package:vector_math/vector_math_64.dart'; + +sealed class InputEvent {} + +enum MouseButton { left, middle, right } + +enum MouseEventType { hover, move, buttonDown, buttonUp } + +class MouseEvent extends InputEvent { + final MouseEventType type; + final MouseButton? button; + final Vector2 localPosition; + final Vector2 delta; + + MouseEvent(this.type, this.button, this.localPosition, this.delta); +} + +enum TouchEventType { + // move, + tap, + doubleTap, +} + +class TouchEvent extends InputEvent { + final TouchEventType type; + final Vector2? localPosition; + final Vector2? delta; + + TouchEvent(this.type, this.localPosition, this.delta); +} + +enum ScaleEventType { start, update, end } + +class ScaleStartEvent extends InputEvent { + final int numPointers; + final ScaleEventType type = ScaleEventType.start; + final (double, double) localFocalPoint; + + ScaleStartEvent({ + required this.numPointers, + required this.localFocalPoint, + }); +} + +class ScaleEndEvent extends InputEvent { + final int numPointers; + final ScaleEventType type = ScaleEventType.end; + + ScaleEndEvent({ + required this.numPointers, + }); +} + +class ScaleUpdateEvent extends InputEvent { + final int numPointers; + final ScaleEventType type = ScaleEventType.update; + final (double, double) localFocalPoint; + final (double, double)? localFocalPointDelta; + final double rotation; + final double scale; + final double horizontalScale; + final double verticalScale; + + ScaleUpdateEvent( + {required this.numPointers, + required this.localFocalPoint, + required this.localFocalPointDelta, + required this.rotation, + required this.scale, + required this.horizontalScale, + required this.verticalScale}); +} + +class ScrollEvent extends InputEvent { + final Vector2 localPosition; + final double delta; + + ScrollEvent({required this.localPosition, required this.delta}); +} + +class KeyEvent extends InputEvent { + final KeyEventType type; + final PhysicalKey key; + + KeyEvent(this.type, this.key); +} + +enum KeyEventType { down, up } + +enum PhysicalKey { W, A, S, D } + +enum InputAction { TRANSLATE, ROTATE, PICK, ZOOM, NONE }