refactor InputHandler interface/DelegateInputHandler implementation

This commit is contained in:
Nick Fisher
2025-05-09 11:18:07 +08:00
parent 7961ed06f7
commit 1ddeac2d31
13 changed files with 1470 additions and 1058 deletions

View File

@@ -1,7 +1,7 @@
library; library;
export 'src/input_types.dart';
export 'src/input_handler.dart'; export 'src/input_handler.dart';
export 'src/delegates.dart';
export 'src/delegate_input_handler.dart'; export 'src/delegate_input_handler.dart';
export 'src/implementations/default_pick_delegate.dart'; export 'src/implementations/default_pick_delegate.dart';
export 'src/implementations/gizmo_pick_delegate.dart'; export 'src/implementations/gizmo_pick_delegate.dart';

View File

@@ -1,301 +1,99 @@
import 'dart:async'; import 'dart:async';
import 'package:logging/logging.dart'; 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 'package:thermion_dart/thermion_dart.dart';
import 'implementations/fixed_orbit_camera_rotation_delegate.dart'; import 'implementations/fixed_orbit_camera_rotation_delegate.dart';
import 'implementations/free_flight_camera_delegate.dart'; import 'implementations/free_flight_camera_delegate.dart';
typedef PointerEventDetails = (Vector2 localPosition, Vector2 delta);
abstract class InputHandlerDelegate {
Future handle(Set<InputEvent> events);
}
///
/// An [InputHandler] that accumulates pointer/key events every frame,
/// delegating the actual update to an [InputHandlerDelegate].
///
class DelegateInputHandler implements InputHandler { class DelegateInputHandler implements InputHandler {
final ThermionViewer viewer; final ThermionViewer viewer;
Stream<List<InputType>> get gestures => _gesturesController.stream; late final _logger = Logger(this.runtimeType.toString());
final _gesturesController = StreamController<List<InputType>>.broadcast();
Stream<Matrix4> get cameraUpdated => _cameraUpdatedController.stream; Stream<List<InputEvent>> get events => _gesturesController.stream;
final _cameraUpdatedController = StreamController<Matrix4>.broadcast();
final _logger = Logger("DelegateInputHandler"); final _gesturesController = StreamController<List<InputEvent>>.broadcast();
final _events = <InputEvent>{};
InputHandlerDelegate? transformDelegate; final List<InputHandlerDelegate> delegates;
PickDelegate? pickDelegate;
final Set<PhysicalKey> _pressedKeys = {};
final _inputDeltas = <InputType, Vector3>{};
Map<InputType, InputAction> _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 = <InputType, Matrix3>{};
void setTransformForAction(InputType inputType, Matrix3 transform) {
_axes[inputType] = transform;
}
DelegateInputHandler({
required this.viewer,
required this.transformDelegate,
this.pickDelegate,
Map<InputType, InputAction>? 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();
}
DelegateInputHandler({required this.viewer, required this.delegates}) {
FilamentApp.instance!.registerRequestFrameHook(process); FilamentApp.instance!.registerRequestFrameHook(process);
} }
factory DelegateInputHandler.fixedOrbit(ThermionViewer viewer, factory DelegateInputHandler.fixedOrbit(ThermionViewer viewer,
{double minimumDistance = 10.0, {double minimumDistance = 0.1,
Vector3? target, Vector3? target,
ThermionEntity? entity, InputSensitivityOptions sensitivity = const InputSensitivityOptions(),
PickDelegate? pickDelegate}) => ThermionEntity? entity}) {
DelegateInputHandler( return DelegateInputHandler(viewer: viewer, delegates: [
viewer: viewer, OrbitInputHandlerDelegate(viewer.view,
pickDelegate: pickDelegate, sensitivity: sensitivity,
transformDelegate: FixedOrbitRotateInputHandlerDelegate(viewer.view, minZoomDistance: minimumDistance,
minimumDistance: minimumDistance), maxZoomDistance: 1000.0)
actions: { ]);
InputType.MMB_HOLD_AND_MOVE: InputAction.ROTATE, }
InputType.SCALE1: InputAction.ROTATE,
InputType.SCALE2: InputAction.ZOOM,
InputType.SCROLLWHEEL: InputAction.ZOOM
});
factory DelegateInputHandler.flight(ThermionViewer viewer, factory DelegateInputHandler.flight(ThermionViewer viewer,
{PickDelegate? pickDelegate, {bool freeLook = false,
bool freeLook = false, InputSensitivityOptions sensitivity = const InputSensitivityOptions(),
double panSensitivity = 0.1,
double zoomSensitivity = 0.1,
double movementSensitivity = 0.1,
double rotateSensitivity = 0.01,
double? clampY,
ThermionEntity? entity}) => ThermionEntity? entity}) =>
DelegateInputHandler( DelegateInputHandler(viewer: viewer, delegates: [
viewer: viewer, FreeFlightInputHandlerDelegateV2(viewer.view, sensitivity: sensitivity)
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,
});
bool _processing = false; bool _processing = false;
Future<void> process() async { Future<void> process() async {
_processing = true; _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 delegate = delegates.first;
} final keyUp = <PhysicalKey, KeyEvent>{};
final keyTypes = <InputType>[]; final keyDown = <PhysicalKey, KeyEvent>{};
for (final key in _pressedKeys) {
InputAction? keyAction;
InputType? keyType = null;
Vector3? vector;
switch (key) { for (final event in _events) {
case PhysicalKey.W: if (event is KeyEvent) {
keyType = InputType.KEYDOWN_W; switch (event.type) {
vector = Vector3(0, 0, -1); case KeyEventType.up:
break; keyUp[event.key] = event;
case PhysicalKey.A: case KeyEventType.down:
keyType = InputType.KEYDOWN_A; keyDown[event.key] = event;
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 key in keyUp.keys) {
_events.remove(keyDown[key]);
_events.remove(keyUp[key]);
} }
var transform = await transformDelegate?.execute(); await delegate.handle(_events);
var updates = _inputDeltas.keys.followedBy(keyTypes).toList();
if (updates.isNotEmpty) {
_gesturesController.add(updates);
}
if (transform != null) {
_cameraUpdatedController.add(transform);
}
_inputDeltas.clear(); _events.clear();
_events.addAll(keyDown.values);
_processing = false; _processing = false;
} }
@override
Future<void> 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<void> 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<void> onPointerUp(bool isMiddle) async {}
@override
Future<void> 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<void> 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 @override
Future dispose() async { Future dispose() async {
FilamentApp.instance!.unregisterRequestFrameHook(process); FilamentApp.instance!.unregisterRequestFrameHook(process);
} }
@override @override
Future<bool> get initialized => viewer.initialized; Future handle(InputEvent event) async {
if (_processing) {
@override return;
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<void> onScaleEnd(int pointerCount, double velocity) async {}
@override
Future<void> onScaleStart(Vector2 localPosition, int pointerCount,
Duration? sourceTimestamp) async {
// noop
}
@override
Future<void> 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");
} }
}
@override _events.add(event);
Stream<Matrix4> get transformUpdated => cameraUpdated; }
} }

View File

@@ -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<Matrix4?> 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();
}

View File

@@ -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<void> _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<void> handle(Set<InputEvent> 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);
}
}

View File

@@ -1,24 +1,24 @@
import 'dart:async'; // import 'dart:async';
import 'package:thermion_dart/thermion_dart.dart'; // import 'package:thermion_dart/thermion_dart.dart';
import 'package:vector_math/vector_math_64.dart'; // import 'package:vector_math/vector_math_64.dart';
class DefaultPickDelegate extends PickDelegate { // class DefaultPickDelegate extends PickDelegate {
final ThermionViewer viewer; // final ThermionViewer viewer;
DefaultPickDelegate(this.viewer); // DefaultPickDelegate(this.viewer);
final _picked = StreamController<ThermionEntity>(); // final _picked = StreamController<ThermionEntity>();
Stream<ThermionEntity> get picked => _picked.stream; // Stream<ThermionEntity> get picked => _picked.stream;
Future dispose() async { // Future dispose() async {
_picked.close(); // _picked.close();
} // }
@override // @override
void pick(Vector2 location) { // void pick(Vector2 location) {
viewer.view.pick(location.x.toInt(), location.y.toInt(), (result) { // viewer.view.pick(location.x.toInt(), location.y.toInt(), (result) {
_picked.sink.add(result.entity); // _picked.sink.add(result.entity);
}); // });
} // }
} // }

View File

@@ -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<void> _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<void> handle(Set<InputEvent> 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;
// }

View File

@@ -1,142 +1,143 @@
import 'dart:async'; // import 'dart:async';
import 'package:vector_math/vector_math_64.dart'; // import 'package:vector_math/vector_math_64.dart';
import '../../../viewer/viewer.dart'; // import '../../../viewer/viewer.dart';
import '../../input.dart'; // import '../../input.dart';
/// // ///
/// An [InputHandlerDelegate] that orbits the camera around a fixed // /// An [InputHandlerDelegate] that orbits the camera around a fixed
/// point. // /// point.
/// // ///
class FixedOrbitRotateInputHandlerDelegate implements InputHandlerDelegate { // class FixedOrbitRotateInputHandlerDelegate implements InputHandlerDelegate {
final View view; // final View view;
final double minimumDistance; // final double minimumDistance;
late final Vector3 target; // late final Vector3 target;
final double rotationSensitivity; // final double rotationSensitivity;
final double zoomSensitivity; // final double zoomSensitivity;
Vector2 _queuedRotationDelta = Vector2.zero(); // Vector2 _queuedRotationDelta = Vector2.zero();
double _queuedZoomDelta = 0.0; // double _queuedZoomDelta = 0.0;
Timer? _updateTimer; // Timer? _updateTimer;
FixedOrbitRotateInputHandlerDelegate( // FixedOrbitRotateInputHandlerDelegate(
this.view, { // this.view, {
Vector3? target, // Vector3? target,
this.minimumDistance = 10.0, // this.minimumDistance = 10.0,
this.rotationSensitivity = 0.01, // this.rotationSensitivity = 0.01,
this.zoomSensitivity = 0.1, // this.zoomSensitivity = 0.1,
}) { // }) {
this.target = target ?? Vector3.zero(); // this.target = target ?? Vector3.zero();
view.getCamera().then((camera) { // view.getCamera().then((camera) {
camera.lookAt(Vector3(0.0, 0, -minimumDistance), // camera.lookAt(Vector3(0.0, 0, -minimumDistance),
focus: this.target, up: Vector3(0.0, 1.0, 0.0)); // focus: this.target, up: Vector3(0.0, 1.0, 0.0));
}); // });
} // }
void dispose() { // void dispose() {
_updateTimer?.cancel(); // _updateTimer?.cancel();
} // }
@override // @override
Future<void> queue(InputAction action, Vector3? delta) async { // Future<void> queue(InputAction action, Vector3? delta) async {
if (delta == null) return; // if (delta == null) return;
switch (action) { // switch (action) {
case InputAction.ROTATE: // case InputAction.ROTATE:
_queuedRotationDelta += Vector2(delta.x, delta.y); // _queuedRotationDelta += Vector2(delta.x, delta.y);
break; // break;
case InputAction.TRANSLATE: // case InputAction.TRANSLATE:
_queuedZoomDelta += delta.z; // _queuedZoomDelta += delta.z;
break; // break;
case InputAction.PICK: // case InputAction.PICK:
break; // break;
case InputAction.NONE: // case InputAction.NONE:
// Do nothing // // Do nothing
break; // break;
case InputAction.ZOOM: // case InputAction.ZOOM:
_queuedZoomDelta += delta.z; // _queuedZoomDelta -= (delta.z - 1.0);
break; // break;
} // }
} // }
bool _executing = false; // bool _executing = false;
@override // @override
Future<Matrix4?> execute() async { // Future<Matrix4?> execute() async {
if (_queuedRotationDelta.length2 == 0.0 && _queuedZoomDelta == 0.0) { // if (_queuedRotationDelta.length2 == 0.0 && _queuedZoomDelta == 0.0) {
return null; // return null;
} // }
if (_executing) { // if (_executing) {
return null; // return null;
} // }
_executing = true; // _executing = true;
final camera = await view.getCamera(); // final camera = await view.getCamera();
var modelMatrix = await camera.getModelMatrix(); // var modelMatrix = await camera.getModelMatrix();
Vector3 currentPosition = modelMatrix.getTranslation();
Vector3 forward = modelMatrix.forward; // Vector3 currentPosition = modelMatrix.getTranslation();
if (forward.length == 0) { // Vector3 forward = modelMatrix.forward;
forward = Vector3(0, 0, -1);
currentPosition = Vector3(0, 0, minimumDistance);
}
Matrix4? updatedModelMatrix = null; // if (forward.length == 0) {
// forward = Vector3(0, 0, -1);
// currentPosition = Vector3(0, 0, minimumDistance);
// }
// Zoom // Matrix4? updatedModelMatrix = null;
if (_queuedZoomDelta != 0.0) {
var newPosition = currentPosition +
(currentPosition - target).scaled(_queuedZoomDelta * zoomSensitivity);
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 // var distToTarget = (newPosition - target).length;
if (distToTarget >= minimumDistance) {
currentPosition = newPosition;
// Calculate view matrix
forward = (currentPosition - target).normalized();
var right = modelMatrix.up.cross(forward).normalized();
var up = forward.cross(right);
Matrix4 newViewMatrix = makeViewMatrix(currentPosition, target, up); // // if we somehow overshot the minimum distance, reset the camera to the minimum distance
newViewMatrix.invert(); // 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); // Matrix4 newViewMatrix = makeViewMatrix(currentPosition, target, up);
updatedModelMatrix = newViewMatrix; // newViewMatrix.invert();
}
} else if (_queuedRotationDelta.length != 0) {
double rotateX = _queuedRotationDelta.x * rotationSensitivity;
double rotateY = _queuedRotationDelta.y * rotationSensitivity;
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 // var modelMatrix = await camera.getModelMatrix();
// 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());
modelMatrix = rot1 * rot2 * modelMatrix; // // for simplicity, we always assume a fixed coordinate system where
await camera.setModelMatrix(modelMatrix); // // we are rotating around world Y and camera X
updatedModelMatrix = modelMatrix; // 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 // modelMatrix = rot1 * rot2 * modelMatrix;
_queuedRotationDelta = Vector2.zero(); // await camera.setModelMatrix(modelMatrix);
_queuedZoomDelta = 0.0; // updatedModelMatrix = modelMatrix;
// }
_executing = false; // // Reset queued deltas
return updatedModelMatrix; // _queuedRotationDelta = Vector2.zero();
} // _queuedZoomDelta = 0.0;
}
// _executing = false;
// return updatedModelMatrix;
// }
// }

View File

@@ -1,133 +1,133 @@
import 'dart:async'; // import 'dart:async';
import 'package:vector_math/vector_math_64.dart'; // import 'package:vector_math/vector_math_64.dart';
import '../../../viewer/viewer.dart'; // import '../../../viewer/viewer.dart';
import '../delegates.dart'; // import '../../input.dart';
import '../input_handler.dart';
class FreeFlightInputHandlerDelegate implements InputHandlerDelegate { // class FreeFlightInputHandlerDelegate implements InputHandlerDelegate {
final View view; // final View view;
final Vector3? minBounds; // final Vector3? minBounds;
final Vector3? maxBounds; // final Vector3? maxBounds;
final double rotationSensitivity; // final double rotationSensitivity;
final double movementSensitivity; // final double movementSensitivity;
final double zoomSensitivity; // final double zoomSensitivity;
final double panSensitivity; // final double panSensitivity;
final double? clampY; // final double? clampY;
Vector2 _queuedRotationDelta = Vector2.zero(); // Vector2 _queuedRotationDelta = Vector2.zero();
Vector3 _queuedTranslateDelta = Vector3.zero(); // Vector3 _queuedTranslateDelta = Vector3.zero();
double _queuedZoomDelta = 0.0; // double _queuedZoomDelta = 0.0;
Vector3 _queuedMoveDelta = Vector3.zero(); // Vector3 _queuedMoveDelta = Vector3.zero();
FreeFlightInputHandlerDelegate(this.view, // FreeFlightInputHandlerDelegate(this.view,
{this.minBounds, // {this.minBounds,
this.maxBounds, // this.maxBounds,
this.rotationSensitivity = 0.001, // this.rotationSensitivity = 0.001,
this.movementSensitivity = 0.1, // this.movementSensitivity = 0.1,
this.zoomSensitivity = 0.1, // this.zoomSensitivity = 0.1,
this.panSensitivity = 0.1, // this.panSensitivity = 0.1,
this.clampY}) {} // this.clampY}) {}
@override // @override
Future<void> queue(InputAction action, Vector3? delta) async { // Future<void> queue(InputAction action, Vector3? delta) async {
if (delta == null) return; // if (delta == null) return;
switch (action) { // switch (action) {
case InputAction.ROTATE: // case InputAction.ROTATE:
_queuedRotationDelta += Vector2(delta.x, delta.y); // _queuedRotationDelta += Vector2(delta.x, delta.y);
break; // break;
case InputAction.TRANSLATE: // case InputAction.TRANSLATE:
_queuedTranslateDelta += delta; // _queuedTranslateDelta += delta;
break; // break;
case InputAction.PICK: // case InputAction.PICK:
_queuedZoomDelta += delta.z; // _queuedZoomDelta += delta.z;
break; // break;
case InputAction.NONE: // case InputAction.NONE:
break; // break;
case InputAction.ZOOM: // case InputAction.ZOOM:
_queuedZoomDelta += delta.z; // _queuedZoomDelta += delta.z;
break; // break;
} // }
} // }
bool _executing = false; // bool _executing = false;
@override // @override
Future<Matrix4?> execute() async { // Future<Matrix4?> execute() async {
if (_executing) { // if (_executing) {
return null; // return null;
} // }
_executing = true; // _executing = true;
if (_queuedRotationDelta.length2 == 0.0 && // if (_queuedRotationDelta.length2 == 0.0 &&
_queuedTranslateDelta.length2 == 0.0 && // _queuedTranslateDelta.length2 == 0.0 &&
_queuedZoomDelta == 0.0 && // _queuedZoomDelta == 0.0 &&
_queuedMoveDelta.length2 == 0.0) { // _queuedMoveDelta.length2 == 0.0) {
_executing = false; // _executing = false;
return null; // 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(); // Vector3 relativeTranslation = Vector3.zero();
Quaternion relativeRotation = Quaternion.identity(); // Quaternion relativeRotation = Quaternion.identity();
if (_queuedRotationDelta.length2 > 0.0) { // if (_queuedRotationDelta.length2 > 0.0) {
double deltaX = _queuedRotationDelta.x * rotationSensitivity; // double deltaX = _queuedRotationDelta.x * rotationSensitivity;
double deltaY = _queuedRotationDelta.y * rotationSensitivity; // double deltaY = _queuedRotationDelta.y * rotationSensitivity;
relativeRotation = Quaternion.axisAngle(current.up, -deltaX) * // relativeRotation = Quaternion.axisAngle(current.up, -deltaX) *
Quaternion.axisAngle(current.right, -deltaY); // Quaternion.axisAngle(current.right, -deltaY);
_queuedRotationDelta = Vector2.zero(); // _queuedRotationDelta = Vector2.zero();
} // }
// Apply (mouse) pan // // Apply (mouse) pan
if (_queuedTranslateDelta.length2 > 0.0) { // if (_queuedTranslateDelta.length2 > 0.0) {
double deltaX = -_queuedTranslateDelta.x * panSensitivity; // double deltaX = -_queuedTranslateDelta.x * panSensitivity;
double deltaY = _queuedTranslateDelta.y * panSensitivity; // double deltaY = _queuedTranslateDelta.y * panSensitivity;
double deltaZ = -_queuedTranslateDelta.z * panSensitivity; // double deltaZ = -_queuedTranslateDelta.z * panSensitivity;
relativeTranslation += current.right * deltaX + // relativeTranslation += current.right * deltaX +
current.up * deltaY + // current.up * deltaY +
current.forward * deltaZ; // current.forward * deltaZ;
_queuedTranslateDelta = Vector3.zero(); // _queuedTranslateDelta = Vector3.zero();
} // }
// Apply zoom // // Apply zoom
if (_queuedZoomDelta != 0.0) { // if (_queuedZoomDelta != 0.0) {
var zoomTranslation = current.forward..scaled(zoomSensitivity); // print("_queuedZoomDelta $_queuedZoomDelta");
zoomTranslation.scale(_queuedZoomDelta); // var zoomTranslation = current.forward..scaled(zoomSensitivity);
relativeTranslation += zoomTranslation; // zoomTranslation.scale(_queuedZoomDelta);
_queuedZoomDelta = 0.0; // relativeTranslation += zoomTranslation;
} // _queuedZoomDelta = 0.0;
// }
// Apply queued movement // // Apply queued movement
if (_queuedMoveDelta.length2 > 0.0) { // if (_queuedMoveDelta.length2 > 0.0) {
relativeTranslation += (current.right * _queuedMoveDelta.x + // relativeTranslation += (current.right * _queuedMoveDelta.x +
current.up * _queuedMoveDelta.y + // current.up * _queuedMoveDelta.y +
current.forward * _queuedMoveDelta.z) * // current.forward * _queuedMoveDelta.z) *
movementSensitivity; // 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 // // // 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 // // // to the entity's translation
// if (await entity != activeCamera.getEntity()) { // // if (await entity != activeCamera.getEntity()) {
// Matrix4 modelMatrix = await activeCamera.getModelMatrix(); // // Matrix4 modelMatrix = await activeCamera.getModelMatrix();
// relativeTranslation = modelMatrix.getRotation() * relativeTranslation; // // relativeTranslation = modelMatrix.getRotation() * relativeTranslation;
// } // // }
var updated = Matrix4.compose( // var updated = Matrix4.compose(
relativeTranslation, relativeRotation, Vector3(1, 1, 1)) * // relativeTranslation, relativeRotation, Vector3(1, 1, 1)) *
current; // current;
await activeCamera.setModelMatrix(updated);
_executing = false; // await activeCamera.setModelMatrix(updated);
return updated;
} // _executing = false;
} // return updated;
// }
// }

View File

@@ -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<void> handle(Set<InputEvent> 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;
}
}

View File

@@ -1,371 +1,371 @@
import 'dart:async'; // import 'dart:async';
import 'dart:math'; // import 'dart:math';
import 'package:thermion_dart/thermion_dart.dart'; // import 'package:thermion_dart/thermion_dart.dart';
class _Gizmo { // class _Gizmo {
final ThermionViewer viewer; // final ThermionViewer viewer;
final GizmoAsset _gizmo; // final GizmoAsset _gizmo;
final transformUpdates = StreamController<({Matrix4 transform})>.broadcast(); // final transformUpdates = StreamController<({Matrix4 transform})>.broadcast();
Axis? _active; // Axis? _active;
final GizmoType type; // final GizmoType type;
_Gizmo(this._gizmo, this.viewer, this.type); // _Gizmo(this._gizmo, this.viewer, this.type);
static Future<_Gizmo> forType(ThermionViewer viewer, GizmoType type) async { // static Future<_Gizmo> forType(ThermionViewer viewer, GizmoType type) async {
final view = await viewer.view; // final view = await viewer.view;
return _Gizmo(await viewer.getGizmo(type), viewer, type); // return _Gizmo(await viewer.getGizmo(type), viewer, type);
} // }
Future dispose() async { // Future dispose() async {
await transformUpdates.close(); // await transformUpdates.close();
await viewer.destroyAsset(_gizmo); // await viewer.destroyAsset(_gizmo);
} // }
Future hide() async { // Future hide() async {
final scene = await viewer.view.getScene(); // final scene = await viewer.view.getScene();
await scene.remove(_gizmo); // await scene.remove(_gizmo);
} // }
Future reveal() async { // Future reveal() async {
final scene = await viewer.view.getScene(); // final scene = await viewer.view.getScene();
await scene.add(_gizmo); // await scene.add(_gizmo);
gizmoTransform = await _gizmo.getWorldTransform(); // gizmoTransform = await _gizmo.getWorldTransform();
} // }
double _getAngleBetweenVectors(Vector2 v1, Vector2 v2) { // double _getAngleBetweenVectors(Vector2 v1, Vector2 v2) {
// Normalize vectors to ensure consistent rotation regardless of distance from center // // Normalize vectors to ensure consistent rotation regardless of distance from center
v1.normalize(); // v1.normalize();
v2.normalize(); // v2.normalize();
// Calculate angle using atan2 // // Calculate angle using atan2
double angle = atan2(v2.y, v2.x) - atan2(v1.y, v1.x); // double angle = atan2(v2.y, v2.x) - atan2(v1.y, v1.x);
// Ensure angle is between -π and π // // Ensure angle is between -π and π
if (angle > pi) angle -= 2 * pi; // if (angle > pi) angle -= 2 * pi;
if (angle < -pi) angle += 2 * pi; // if (angle < -pi) angle += 2 * pi;
return angle; // return angle;
} // }
void checkHover(int x, int y) async { // void checkHover(int x, int y) async {
_gizmo.pick(x, y, handler: (result, coords) async { // _gizmo.pick(x, y, handler: (result, coords) async {
switch (result) { // switch (result) {
case GizmoPickResultType.None: // case GizmoPickResultType.None:
await _gizmo.unhighlight(); // await _gizmo.unhighlight();
_active = null; // _active = null;
break; // break;
case GizmoPickResultType.AxisX: // case GizmoPickResultType.AxisX:
_active = Axis.X; // _active = Axis.X;
case GizmoPickResultType.AxisY: // case GizmoPickResultType.AxisY:
_active = Axis.Y; // _active = Axis.Y;
case GizmoPickResultType.AxisZ: // case GizmoPickResultType.AxisZ:
_active = Axis.Z; // _active = Axis.Z;
default: // default:
} // }
}); // });
} // }
Matrix4? gizmoTransform; // Matrix4? gizmoTransform;
void _updateTransform(Vector2 currentPosition, Vector2 delta) async { // void _updateTransform(Vector2 currentPosition, Vector2 delta) async {
if (type == GizmoType.translation) { // if (type == GizmoType.translation) {
await _updateTranslation(currentPosition, delta); // await _updateTranslation(currentPosition, delta);
} else if (type == GizmoType.rotation) { // } else if (type == GizmoType.rotation) {
await _updateRotation(currentPosition, delta); // await _updateRotation(currentPosition, delta);
} // }
await _gizmo.setTransform(gizmoTransform!); // await _gizmo.setTransform(gizmoTransform!);
transformUpdates.add((transform: gizmoTransform!)); // transformUpdates.add((transform: gizmoTransform!));
} // }
Future<void>? _updateTranslation( // Future<void>? _updateTranslation(
Vector2 currentPosition, Vector2 delta) async { // Vector2 currentPosition, Vector2 delta) async {
var view = await viewer.view; // var view = await viewer.view;
var camera = await viewer.getActiveCamera(); // var camera = await viewer.getActiveCamera();
var viewport = await view.getViewport(); // var viewport = await view.getViewport();
var projectionMatrix = await camera.getProjectionMatrix(); // var projectionMatrix = await camera.getProjectionMatrix();
var viewMatrix = await camera.getViewMatrix(); // var viewMatrix = await camera.getViewMatrix();
var inverseViewMatrix = await camera.getModelMatrix(); // var inverseViewMatrix = await camera.getModelMatrix();
var inverseProjectionMatrix = projectionMatrix.clone()..invert(); // var inverseProjectionMatrix = projectionMatrix.clone()..invert();
// get gizmo position in screenspace // // get gizmo position in screenspace
var gizmoPositionWorldSpace = gizmoTransform!.getTranslation(); // var gizmoPositionWorldSpace = gizmoTransform!.getTranslation();
Vector4 gizmoClipSpace = projectionMatrix * // Vector4 gizmoClipSpace = projectionMatrix *
viewMatrix * // viewMatrix *
Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y, // Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y,
gizmoPositionWorldSpace.z, 1.0); // gizmoPositionWorldSpace.z, 1.0);
var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w; // var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w;
var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width, // var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width,
viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height)); // viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height));
gizmoScreenSpace += delta; // gizmoScreenSpace += delta;
gizmoNdc = Vector4(((gizmoScreenSpace.x / viewport.width) - 0.5) * 2, // gizmoNdc = Vector4(((gizmoScreenSpace.x / viewport.width) - 0.5) * 2,
(((gizmoScreenSpace.y / viewport.height)) - 0.5) * -2, gizmoNdc.z, 1.0); // (((gizmoScreenSpace.y / viewport.height)) - 0.5) * -2, gizmoNdc.z, 1.0);
var gizmoViewSpace = inverseProjectionMatrix * gizmoNdc; // var gizmoViewSpace = inverseProjectionMatrix * gizmoNdc;
gizmoViewSpace /= gizmoViewSpace.w; // gizmoViewSpace /= gizmoViewSpace.w;
var newPosition = (inverseViewMatrix * gizmoViewSpace).xyz; // var newPosition = (inverseViewMatrix * gizmoViewSpace).xyz;
Vector3 worldSpaceDelta = newPosition - gizmoTransform!.getTranslation(); // Vector3 worldSpaceDelta = newPosition - gizmoTransform!.getTranslation();
worldSpaceDelta.multiply(_active!.asVector()); // worldSpaceDelta.multiply(_active!.asVector());
gizmoTransform! // gizmoTransform!
.setTranslation(gizmoTransform!.getTranslation() + worldSpaceDelta); // .setTranslation(gizmoTransform!.getTranslation() + worldSpaceDelta);
} // }
Future<void>? _updateRotation(Vector2 currentPosition, Vector2 delta) async { // Future<void>? _updateRotation(Vector2 currentPosition, Vector2 delta) async {
var camera = await viewer.view.getCamera(); // var camera = await viewer.view.getCamera();
var viewport = await viewer.view.getViewport(); // var viewport = await viewer.view.getViewport();
var projectionMatrix = await camera.getProjectionMatrix(); // var projectionMatrix = await camera.getProjectionMatrix();
var viewMatrix = await camera.getViewMatrix(); // var viewMatrix = await camera.getViewMatrix();
// Get gizmo center in screen space // // Get gizmo center in screen space
var gizmoPositionWorldSpace = gizmoTransform!.getTranslation(); // var gizmoPositionWorldSpace = gizmoTransform!.getTranslation();
Vector4 gizmoClipSpace = projectionMatrix * // Vector4 gizmoClipSpace = projectionMatrix *
viewMatrix * // viewMatrix *
Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y, // Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y,
gizmoPositionWorldSpace.z, 1.0); // gizmoPositionWorldSpace.z, 1.0);
var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w; // var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w;
var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width, // var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width,
viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height)); // viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height));
// Calculate vectors from gizmo center to previous and current mouse positions // // Calculate vectors from gizmo center to previous and current mouse positions
var prevVector = (currentPosition - delta) - gizmoScreenSpace; // var prevVector = (currentPosition - delta) - gizmoScreenSpace;
var currentVector = currentPosition - gizmoScreenSpace; // var currentVector = currentPosition - gizmoScreenSpace;
// Calculate rotation angle based on the active axis // // Calculate rotation angle based on the active axis
double rotationAngle = 0.0; // double rotationAngle = 0.0;
switch (_active) { // switch (_active) {
case Axis.X: // case Axis.X:
// For X axis, project onto YZ plane // // For X axis, project onto YZ plane
var prev = Vector2(prevVector.y, -prevVector.x); // var prev = Vector2(prevVector.y, -prevVector.x);
var curr = Vector2(currentVector.y, -currentVector.x); // var curr = Vector2(currentVector.y, -currentVector.x);
rotationAngle = _getAngleBetweenVectors(prev, curr); // rotationAngle = _getAngleBetweenVectors(prev, curr);
break; // break;
case Axis.Y: // case Axis.Y:
// For Y axis, project onto XZ plane // // For Y axis, project onto XZ plane
var prev = Vector2(prevVector.x, -prevVector.y); // var prev = Vector2(prevVector.x, -prevVector.y);
var curr = Vector2(currentVector.x, -currentVector.y); // var curr = Vector2(currentVector.x, -currentVector.y);
rotationAngle = _getAngleBetweenVectors(prev, curr); // rotationAngle = _getAngleBetweenVectors(prev, curr);
break; // break;
case Axis.Z: // case Axis.Z:
// For Z axis, use screen plane directly // // For Z axis, use screen plane directly
rotationAngle = -1 * _getAngleBetweenVectors(prevVector, currentVector); // rotationAngle = -1 * _getAngleBetweenVectors(prevVector, currentVector);
break; // break;
default: // default:
return; // return;
} // }
// Create rotation matrix based on the active axis // // Create rotation matrix based on the active axis
var rotationMatrix = Matrix4.identity(); // var rotationMatrix = Matrix4.identity();
switch (_active) { // switch (_active) {
case Axis.X: // case Axis.X:
rotationMatrix.setRotationX(rotationAngle); // rotationMatrix.setRotationX(rotationAngle);
break; // break;
case Axis.Y: // case Axis.Y:
rotationMatrix.setRotationY(rotationAngle); // rotationMatrix.setRotationY(rotationAngle);
break; // break;
case Axis.Z: // case Axis.Z:
rotationMatrix.setRotationZ(rotationAngle); // rotationMatrix.setRotationZ(rotationAngle);
break; // break;
default: // default:
return; // return;
} // }
// Apply rotation to the current transform // // Apply rotation to the current transform
gizmoTransform = gizmoTransform! * rotationMatrix; // gizmoTransform = gizmoTransform! * rotationMatrix;
} // }
} // }
class GizmoInputHandler extends InputHandler { // class GizmoInputHandler extends InputHandler {
final ThermionViewer viewer; // final ThermionViewer viewer;
late final _gizmos = <GizmoType, _Gizmo>{}; // late final _gizmos = <GizmoType, _Gizmo>{};
_Gizmo? _active; // _Gizmo? _active;
ThermionEntity? _attached; // ThermionEntity? _attached;
Future attach(ThermionEntity entity) async { // Future attach(ThermionEntity entity) async {
if (_attached != null) { // if (_attached != null) {
await detach(); // await detach();
} // }
_attached = entity; // _attached = entity;
if (_active != null) { // if (_active != null) {
await FilamentApp.instance!.setParent(_attached!, _active!._gizmo.entity); // await FilamentApp.instance!.setParent(_attached!, _active!._gizmo.entity);
await _active!.reveal(); // await _active!.reveal();
} // }
} // }
Future<Matrix4?> getGizmoTransform() async { // Future<Matrix4?> getGizmoTransform() async {
return _active?.gizmoTransform; // return _active?.gizmoTransform;
} // }
Future detach() async { // Future detach() async {
if (_attached == null) { // if (_attached == null) {
return; // return;
} // }
await FilamentApp.instance!.setParent(_attached!, null); // await FilamentApp.instance!.setParent(_attached!, null);
await _active?.hide(); // await _active?.hide();
_attached = null; // _attached = null;
} // }
final _initialized = Completer<bool>(); // final _initialized = Completer<bool>();
final _transformController = StreamController<Matrix4>.broadcast(); // final _transformController = StreamController<Matrix4>.broadcast();
Stream<Matrix4> get transformUpdated => _transformController.stream; // Stream<Matrix4> get transformUpdated => _transformController.stream;
final _pickResultController = StreamController<ThermionEntity?>.broadcast(); // final _pickResultController = StreamController<ThermionEntity?>.broadcast();
Stream<ThermionEntity?> get onPickResult => _pickResultController.stream; // Stream<ThermionEntity?> get onPickResult => _pickResultController.stream;
GizmoInputHandler({required this.viewer, required GizmoType initialType}) { // GizmoInputHandler({required this.viewer, required GizmoType initialType}) {
initialize().then((_) { // initialize().then((_) {
setGizmoType(initialType); // setGizmoType(initialType);
}); // });
} // }
GizmoType? getGizmoType() { // GizmoType? getGizmoType() {
return _active?.type; // return _active?.type;
} // }
Future setGizmoType(GizmoType? type) async { // Future setGizmoType(GizmoType? type) async {
if (type == null) { // if (type == null) {
await detach(); // await detach();
_active?.hide(); // _active?.hide();
_active = null; // _active = null;
} else { // } else {
_active?.hide(); // _active?.hide();
_active = _gizmos[type]!; // _active = _gizmos[type]!;
_active!.reveal(); // _active!.reveal();
if (_attached != null) { // if (_attached != null) {
await attach(_attached!); // await attach(_attached!);
} // }
} // }
} // }
Future initialize() async { // Future initialize() async {
if (_initialized.isCompleted) { // if (_initialized.isCompleted) {
throw Exception("Already initialized"); // throw Exception("Already initialized");
} // }
await viewer.initialized; // await viewer.initialized;
_gizmos[GizmoType.translation] = // _gizmos[GizmoType.translation] =
await _Gizmo.forType(viewer, GizmoType.translation); // await _Gizmo.forType(viewer, GizmoType.translation);
_gizmos[GizmoType.rotation] = // _gizmos[GizmoType.rotation] =
await _Gizmo.forType(viewer, GizmoType.rotation); // await _Gizmo.forType(viewer, GizmoType.rotation);
await setGizmoType(GizmoType.translation); // await setGizmoType(GizmoType.translation);
for (final gizmo in _gizmos.values) { // for (final gizmo in _gizmos.values) {
gizmo.transformUpdates.stream.listen((update) { // gizmo.transformUpdates.stream.listen((update) {
_transformController.add(update.transform); // _transformController.add(update.transform);
}); // });
} // }
_initialized.complete(true); // _initialized.complete(true);
} // }
@override // @override
Future dispose() async { // Future dispose() async {
_gizmos[GizmoType.rotation]!.dispose(); // _gizmos[GizmoType.rotation]!.dispose();
_gizmos[GizmoType.translation]!.dispose(); // _gizmos[GizmoType.translation]!.dispose();
_gizmos.clear(); // _gizmos.clear();
} // }
@override // @override
InputAction? getActionForType(InputType gestureType) { // InputAction? getActionForType(InputType gestureType) {
if (gestureType == InputType.LMB_DOWN) { // if (gestureType == InputType.LMB_DOWN) {
return InputAction.PICK; // return InputAction.PICK;
} // }
throw UnimplementedError(); // throw UnimplementedError();
} // }
@override // @override
Future<bool> get initialized => _initialized.future; // Future<bool> get initialized => _initialized.future;
@override // @override
void keyDown(PhysicalKey key) {} // void keyDown(PhysicalKey key) {}
@override // @override
void keyUp(PhysicalKey key) {} // void keyUp(PhysicalKey key) {}
@override // @override
Future<void>? onPointerDown(Vector2 localPosition, bool isMiddle) async { // Future<void>? onPointerDown(Vector2 localPosition, bool isMiddle) async {
if (!_initialized.isCompleted) { // if (!_initialized.isCompleted) {
return; // return;
} // }
if (isMiddle) { // if (isMiddle) {
return; // return;
} // }
await viewer.view.pick(localPosition.x.toInt(), localPosition.y.toInt(), // await viewer.view.pick(localPosition.x.toInt(), localPosition.y.toInt(),
(result) async { // (result) async {
if (_active?._gizmo.isNonPickable(result.entity) == true || // if (_active?._gizmo.isNonPickable(result.entity) == true ||
result.entity == FILAMENT_ENTITY_NULL) { // result.entity == FILAMENT_ENTITY_NULL) {
_pickResultController.add(null); // _pickResultController.add(null);
return; // return;
} // }
if (_active?._gizmo.isGizmoEntity(result.entity) != true) { // if (_active?._gizmo.isGizmoEntity(result.entity) != true) {
_pickResultController.add(result.entity); // _pickResultController.add(result.entity);
} // }
}); // });
} // }
@override // @override
Future<void>? onPointerHover(Vector2 localPosition, Vector2 delta) async { // Future<void>? onPointerHover(Vector2 localPosition, Vector2 delta) async {
if (!_initialized.isCompleted) { // if (!_initialized.isCompleted) {
return; // return;
} // }
_active?.checkHover(localPosition.x.floor(), localPosition.y.floor()); // _active?.checkHover(localPosition.x.floor(), localPosition.y.floor());
} // }
@override // @override
Future<void>? onPointerMove( // Future<void>? onPointerMove(
Vector2 localPosition, Vector2 delta, bool isMiddle) async { // Vector2 localPosition, Vector2 delta, bool isMiddle) async {
if (!isMiddle && _active?._active != null) { // if (!isMiddle && _active?._active != null) {
final scaledDelta = Vector2( // final scaledDelta = Vector2(
delta.x, // delta.x,
delta.y, // delta.y,
); // );
_active!._updateTransform(localPosition, scaledDelta); // _active!._updateTransform(localPosition, scaledDelta);
return; // return;
} // }
} // }
@override // @override
Future<void>? onPointerScroll( // Future<void>? onPointerScroll(
Vector2 localPosition, double scrollDelta) async {} // Vector2 localPosition, double scrollDelta) async {}
@override // @override
Future<void>? onPointerUp(bool isMiddle) async {} // Future<void>? onPointerUp(bool isMiddle) async {}
@override // @override
Future<void>? onScaleEnd(int pointerCount, double velocity) {} // Future<void>? onScaleEnd(int pointerCount, double velocity) {}
@override // @override
Future<void>? onScaleStart( // Future<void>? onScaleStart(
Vector2 focalPoint, int pointerCount, Duration? sourceTimestamp) {} // Vector2 focalPoint, int pointerCount, Duration? sourceTimestamp) {}
@override // @override
Future<void>? onScaleUpdate( // Future<void>? onScaleUpdate(
Vector2 focalPoint, // Vector2 focalPoint,
Vector2 focalPointDelta, // Vector2 focalPointDelta,
double horizontalScale, // double horizontalScale,
double verticalScale, // double verticalScale,
double scale, // double scale,
int pointerCount, // int pointerCount,
double rotation, // double rotation,
Duration? sourceTimestamp) {} // Duration? sourceTimestamp) {}
@override // @override
void setActionForType(InputType gestureType, InputAction gestureAction) { // void setActionForType(InputType gestureType, InputAction gestureAction) {
throw UnimplementedError(); // throw UnimplementedError();
} // }
} // }

View File

@@ -1,121 +1,120 @@
import 'dart:async'; // import 'dart:async';
import 'dart:math'; // import 'dart:math';
import 'package:vector_math/vector_math_64.dart'; // import 'package:vector_math/vector_math_64.dart';
import '../../../viewer/viewer.dart'; // import '../../../viewer/viewer.dart';
import '../delegates.dart'; // import '../../input.dart';
import '../input_handler.dart';
class OverTheShoulderCameraDelegate implements InputHandlerDelegate { // class OverTheShoulderCameraDelegate implements InputHandlerDelegate {
final ThermionViewer viewer; // final ThermionViewer viewer;
late ThermionAsset player; // late ThermionAsset player;
late Camera camera; // late Camera camera;
final double rotationSensitivity; // final double rotationSensitivity;
final double movementSensitivity; // final double movementSensitivity;
final double zoomSensitivity; // final double zoomSensitivity;
final double panSensitivity; // final double panSensitivity;
final double? clampY; // final double? clampY;
static final _up = Vector3(0, 1, 0); // static final _up = Vector3(0, 1, 0);
static final _forward = Vector3(0, 0, -1); // static final _forward = Vector3(0, 0, -1);
static final Vector3 _right = Vector3(1, 0, 0); // static final Vector3 _right = Vector3(1, 0, 0);
Vector2 _queuedRotationDelta = Vector2.zero(); // Vector2 _queuedRotationDelta = Vector2.zero();
double _queuedZoomDelta = 0.0; // double _queuedZoomDelta = 0.0;
Vector3 _queuedMoveDelta = Vector3.zero(); // Vector3 _queuedMoveDelta = Vector3.zero();
final cameraPosition = Vector3(-0.5, 2.5, -3); // final cameraPosition = Vector3(-0.5, 2.5, -3);
final cameraUp = Vector3(0, 1, 0); // final cameraUp = Vector3(0, 1, 0);
var cameraLookAt = Vector3(0, 0.5, 3); // 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, // OverTheShoulderCameraDelegate(this.viewer, this.player, this.camera,
{this.rotationSensitivity = 0.001, // {this.rotationSensitivity = 0.001,
this.movementSensitivity = 0.1, // this.movementSensitivity = 0.1,
this.zoomSensitivity = 0.1, // this.zoomSensitivity = 0.1,
this.panSensitivity = 0.1, // this.panSensitivity = 0.1,
this.clampY, // this.clampY,
ThermionEntity? entity, // ThermionEntity? entity,
this.onUpdate}) {} // this.onUpdate}) {}
@override // @override
Future<void> queue(InputAction action, Vector3? delta) async { // Future<void> queue(InputAction action, Vector3? delta) async {
if (delta == null) return; // if (delta == null) return;
switch (action) { // switch (action) {
case InputAction.ROTATE: // case InputAction.ROTATE:
_queuedRotationDelta += Vector2(delta.x, delta.y); // _queuedRotationDelta += Vector2(delta.x, delta.y);
break; // break;
case InputAction.TRANSLATE: // case InputAction.TRANSLATE:
_queuedMoveDelta += delta; // _queuedMoveDelta += delta;
break; // break;
case InputAction.PICK: // case InputAction.PICK:
_queuedZoomDelta += delta.z; // _queuedZoomDelta += delta.z;
break; // break;
case InputAction.NONE: // case InputAction.NONE:
break; // break;
case InputAction.ZOOM: // case InputAction.ZOOM:
break; // break;
} // }
} // }
static bool _executing = false; // static bool _executing = false;
static bool get executing => _executing; // static bool get executing => _executing;
@override // @override
Future<Matrix4?> execute() async { // Future<Matrix4?> execute() async {
if (_executing) { // if (_executing) {
return null; // return null;
} // }
_executing = true; // _executing = true;
if (_queuedRotationDelta.length2 == 0.0 && // if (_queuedRotationDelta.length2 == 0.0 &&
_queuedZoomDelta == 0.0 && // _queuedZoomDelta == 0.0 &&
_queuedMoveDelta.length2 == 0.0) { // _queuedMoveDelta.length2 == 0.0) {
_executing = false; // _executing = false;
return null; // return null;
} // }
Matrix4 currentPlayerTransform = await player.getWorldTransform(); // Matrix4 currentPlayerTransform = await player.getWorldTransform();
// first we need to convert the move vector to player space // // first we need to convert the move vector to player space
var newTransform = // var newTransform =
Matrix4.translation(_queuedMoveDelta * movementSensitivity); // Matrix4.translation(_queuedMoveDelta * movementSensitivity);
_queuedMoveDelta = Vector3.zero(); // _queuedMoveDelta = Vector3.zero();
Matrix4 newPlayerTransform = newTransform * currentPlayerTransform; // Matrix4 newPlayerTransform = newTransform * currentPlayerTransform;
await player.setTransform(newPlayerTransform); // await player.setTransform(newPlayerTransform);
if (_queuedZoomDelta != 0.0) { // if (_queuedZoomDelta != 0.0) {
// Ignore zoom // // Ignore zoom
} // }
var inverted = newPlayerTransform.clone()..invert(); // var inverted = newPlayerTransform.clone()..invert();
// camera is always looking at -Z, whereas models generally face towards +Z // // camera is always looking at -Z, whereas models generally face towards +Z
if (_queuedRotationDelta.length2 > 0.0) { // if (_queuedRotationDelta.length2 > 0.0) {
double deltaX = _queuedRotationDelta.x * rotationSensitivity; // double deltaX = _queuedRotationDelta.x * rotationSensitivity;
double deltaY = _queuedRotationDelta.y * rotationSensitivity; // double deltaY = _queuedRotationDelta.y * rotationSensitivity;
cameraLookAt = Matrix4.rotationY(-deltaX) * // cameraLookAt = Matrix4.rotationY(-deltaX) *
Matrix4.rotationX(-deltaY) * // Matrix4.rotationX(-deltaY) *
cameraLookAt; // cameraLookAt;
_queuedRotationDelta = Vector2.zero(); // _queuedRotationDelta = Vector2.zero();
} // }
var newCameraViewMatrix = // var newCameraViewMatrix =
makeViewMatrix(cameraPosition, cameraLookAt, cameraUp); // makeViewMatrix(cameraPosition, cameraLookAt, cameraUp);
newCameraViewMatrix.invert(); // newCameraViewMatrix.invert();
var newCameraTransform = newPlayerTransform * newCameraViewMatrix; // var newCameraTransform = newPlayerTransform * newCameraViewMatrix;
await camera.setTransform(newCameraTransform); // await camera.setTransform(newCameraTransform);
// await viewer.queueTransformUpdates( // // await viewer.queueTransformUpdates(
// [camera.getEntity(), player], [newCameraTransform, newPlayerTransform]); // // [camera.getEntity(), player], [newCameraTransform, newPlayerTransform]);
onUpdate?.call(newPlayerTransform); // onUpdate?.call(newPlayerTransform);
_executing = false; // _executing = false;
return newCameraTransform; // return newCameraTransform;
} // }
} // }

View File

@@ -1,64 +1,32 @@
import 'dart:async'; import 'dart:async';
import 'package:vector_math/vector_math_64.dart'; import 'input_types.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 }
///
/// An interface for handling user device input events.
///
abstract class InputHandler { abstract class InputHandler {
///
@Deprecated("Use @transformUpdated instead") ///
Stream get cameraUpdated => transformUpdated; ///
Future? handle(InputEvent event);
Stream<Matrix4> 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<bool> get initialized;
Future dispose(); Future dispose();
}
void setActionForType(InputType gestureType, InputAction gestureAction);
InputAction? getActionForType(InputType gestureType); class InputSensitivityOptions {
final double touchSensitivity;
void keyDown(PhysicalKey key); final double touchScaleSensitivity;
void keyUp(PhysicalKey key); 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});
} }

View File

@@ -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 }