refactor InputHandler interface/DelegateInputHandler implementation
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
// }
|
||||||
@@ -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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -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});
|
||||||
}
|
}
|
||||||
|
|||||||
92
thermion_dart/lib/src/input/src/input_types.dart
Normal file
92
thermion_dart/lib/src/input/src/input_types.dart
Normal 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 }
|
||||||
Reference in New Issue
Block a user