refactor InputHandler interface/DelegateInputHandler implementation
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
library;
|
||||
|
||||
export 'src/input_types.dart';
|
||||
export 'src/input_handler.dart';
|
||||
export 'src/delegates.dart';
|
||||
export 'src/delegate_input_handler.dart';
|
||||
export 'src/implementations/default_pick_delegate.dart';
|
||||
export 'src/implementations/gizmo_pick_delegate.dart';
|
||||
|
||||
@@ -1,301 +1,99 @@
|
||||
import 'dart:async';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_dart/src/input/src/implementations/fixed_orbit_camera_delegate_v2.dart';
|
||||
import 'package:thermion_dart/src/input/src/implementations/free_flight_camera_delegate_v2.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'implementations/fixed_orbit_camera_rotation_delegate.dart';
|
||||
import 'implementations/free_flight_camera_delegate.dart';
|
||||
|
||||
typedef PointerEventDetails = (Vector2 localPosition, Vector2 delta);
|
||||
|
||||
abstract class InputHandlerDelegate {
|
||||
Future handle(Set<InputEvent> events);
|
||||
}
|
||||
|
||||
///
|
||||
/// An [InputHandler] that accumulates pointer/key events every frame,
|
||||
/// delegating the actual update to an [InputHandlerDelegate].
|
||||
///
|
||||
class DelegateInputHandler implements InputHandler {
|
||||
final ThermionViewer viewer;
|
||||
|
||||
Stream<List<InputType>> get gestures => _gesturesController.stream;
|
||||
final _gesturesController = StreamController<List<InputType>>.broadcast();
|
||||
late final _logger = Logger(this.runtimeType.toString());
|
||||
|
||||
Stream<Matrix4> get cameraUpdated => _cameraUpdatedController.stream;
|
||||
final _cameraUpdatedController = StreamController<Matrix4>.broadcast();
|
||||
Stream<List<InputEvent>> get events => _gesturesController.stream;
|
||||
|
||||
final _logger = Logger("DelegateInputHandler");
|
||||
|
||||
InputHandlerDelegate? transformDelegate;
|
||||
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();
|
||||
}
|
||||
final _gesturesController = StreamController<List<InputEvent>>.broadcast();
|
||||
final _events = <InputEvent>{};
|
||||
final List<InputHandlerDelegate> delegates;
|
||||
|
||||
DelegateInputHandler({required this.viewer, required this.delegates}) {
|
||||
FilamentApp.instance!.registerRequestFrameHook(process);
|
||||
}
|
||||
|
||||
factory DelegateInputHandler.fixedOrbit(ThermionViewer viewer,
|
||||
{double minimumDistance = 10.0,
|
||||
Vector3? target,
|
||||
ThermionEntity? entity,
|
||||
PickDelegate? pickDelegate}) =>
|
||||
DelegateInputHandler(
|
||||
viewer: viewer,
|
||||
pickDelegate: pickDelegate,
|
||||
transformDelegate: FixedOrbitRotateInputHandlerDelegate(viewer.view,
|
||||
minimumDistance: minimumDistance),
|
||||
actions: {
|
||||
InputType.MMB_HOLD_AND_MOVE: InputAction.ROTATE,
|
||||
InputType.SCALE1: InputAction.ROTATE,
|
||||
InputType.SCALE2: InputAction.ZOOM,
|
||||
InputType.SCROLLWHEEL: InputAction.ZOOM
|
||||
});
|
||||
{double minimumDistance = 0.1,
|
||||
Vector3? target,
|
||||
InputSensitivityOptions sensitivity = const InputSensitivityOptions(),
|
||||
ThermionEntity? entity}) {
|
||||
return DelegateInputHandler(viewer: viewer, delegates: [
|
||||
OrbitInputHandlerDelegate(viewer.view,
|
||||
sensitivity: sensitivity,
|
||||
minZoomDistance: minimumDistance,
|
||||
maxZoomDistance: 1000.0)
|
||||
]);
|
||||
}
|
||||
|
||||
factory DelegateInputHandler.flight(ThermionViewer viewer,
|
||||
{PickDelegate? pickDelegate,
|
||||
bool freeLook = false,
|
||||
double panSensitivity = 0.1,
|
||||
double zoomSensitivity = 0.1,
|
||||
double movementSensitivity = 0.1,
|
||||
double rotateSensitivity = 0.01,
|
||||
double? clampY,
|
||||
{bool freeLook = false,
|
||||
InputSensitivityOptions sensitivity = const InputSensitivityOptions(),
|
||||
ThermionEntity? entity}) =>
|
||||
DelegateInputHandler(
|
||||
viewer: viewer,
|
||||
pickDelegate: pickDelegate,
|
||||
transformDelegate: FreeFlightInputHandlerDelegate(viewer.view,
|
||||
clampY: clampY,
|
||||
rotationSensitivity: rotateSensitivity,
|
||||
zoomSensitivity: zoomSensitivity,
|
||||
panSensitivity: panSensitivity,
|
||||
movementSensitivity: movementSensitivity),
|
||||
actions: {
|
||||
InputType.MMB_HOLD_AND_MOVE: InputAction.ROTATE,
|
||||
InputType.SCROLLWHEEL: InputAction.ZOOM,
|
||||
InputType.LMB_HOLD_AND_MOVE: InputAction.TRANSLATE,
|
||||
InputType.KEYDOWN_A: InputAction.TRANSLATE,
|
||||
InputType.KEYDOWN_W: InputAction.TRANSLATE,
|
||||
InputType.KEYDOWN_S: InputAction.TRANSLATE,
|
||||
InputType.KEYDOWN_D: InputAction.TRANSLATE,
|
||||
InputType.SCALE1: InputAction.TRANSLATE,
|
||||
InputType.SCALE2: InputAction.ZOOM,
|
||||
if (freeLook) InputType.POINTER_MOVE: InputAction.ROTATE,
|
||||
});
|
||||
DelegateInputHandler(viewer: viewer, delegates: [
|
||||
FreeFlightInputHandlerDelegateV2(viewer.view, sensitivity: sensitivity)
|
||||
]);
|
||||
|
||||
bool _processing = false;
|
||||
|
||||
Future<void> process() async {
|
||||
_processing = true;
|
||||
for (var gestureType in _inputDeltas.keys) {
|
||||
var vector = _inputDeltas[gestureType]!;
|
||||
var action = _actions[gestureType];
|
||||
if (action == null) {
|
||||
continue;
|
||||
}
|
||||
final transform = _axes[gestureType];
|
||||
if (transform != null) {
|
||||
vector = transform * vector;
|
||||
}
|
||||
|
||||
await transformDelegate?.queue(action, vector);
|
||||
}
|
||||
final keyTypes = <InputType>[];
|
||||
for (final key in _pressedKeys) {
|
||||
InputAction? keyAction;
|
||||
InputType? keyType = null;
|
||||
Vector3? vector;
|
||||
final delegate = delegates.first;
|
||||
final keyUp = <PhysicalKey, KeyEvent>{};
|
||||
final keyDown = <PhysicalKey, KeyEvent>{};
|
||||
|
||||
switch (key) {
|
||||
case PhysicalKey.W:
|
||||
keyType = InputType.KEYDOWN_W;
|
||||
vector = Vector3(0, 0, -1);
|
||||
break;
|
||||
case PhysicalKey.A:
|
||||
keyType = InputType.KEYDOWN_A;
|
||||
vector = Vector3(-1, 0, 0);
|
||||
break;
|
||||
case PhysicalKey.S:
|
||||
keyType = InputType.KEYDOWN_S;
|
||||
vector = Vector3(0, 0, 1);
|
||||
break;
|
||||
case PhysicalKey.D:
|
||||
keyType = InputType.KEYDOWN_D;
|
||||
vector = Vector3(1, 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// ignore: unnecessary_null_comparison
|
||||
if (keyType != null) {
|
||||
keyAction = _actions[keyType];
|
||||
|
||||
if (keyAction != null) {
|
||||
var transform = _axes[keyAction];
|
||||
if (transform != null) {
|
||||
vector = transform * vector;
|
||||
}
|
||||
transformDelegate?.queue(keyAction, vector!);
|
||||
keyTypes.add(keyType);
|
||||
for (final event in _events) {
|
||||
if (event is KeyEvent) {
|
||||
switch (event.type) {
|
||||
case KeyEventType.up:
|
||||
keyUp[event.key] = event;
|
||||
case KeyEventType.down:
|
||||
keyDown[event.key] = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final key in keyUp.keys) {
|
||||
_events.remove(keyDown[key]);
|
||||
_events.remove(keyUp[key]);
|
||||
}
|
||||
|
||||
var transform = await transformDelegate?.execute();
|
||||
var updates = _inputDeltas.keys.followedBy(keyTypes).toList();
|
||||
if (updates.isNotEmpty) {
|
||||
_gesturesController.add(updates);
|
||||
}
|
||||
if (transform != null) {
|
||||
_cameraUpdatedController.add(transform);
|
||||
}
|
||||
await delegate.handle(_events);
|
||||
|
||||
_inputDeltas.clear();
|
||||
_events.clear();
|
||||
_events.addAll(keyDown.values);
|
||||
_processing = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<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
|
||||
Future dispose() async {
|
||||
FilamentApp.instance!.unregisterRequestFrameHook(process);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get initialized => viewer.initialized;
|
||||
|
||||
@override
|
||||
void setActionForType(InputType gestureType, InputAction gestureAction) {
|
||||
_actions[gestureType] = gestureAction;
|
||||
}
|
||||
|
||||
@override
|
||||
InputAction? getActionForType(InputType gestureType) {
|
||||
return _actions[gestureType];
|
||||
}
|
||||
|
||||
void keyDown(PhysicalKey key) {
|
||||
_pressedKeys.add(key);
|
||||
}
|
||||
|
||||
void keyUp(PhysicalKey key) {
|
||||
_pressedKeys.remove(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<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");
|
||||
Future handle(InputEvent event) async {
|
||||
if (_processing) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Matrix4> get transformUpdated => cameraUpdated;
|
||||
_events.add(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:vector_math/vector_math_64.dart';
|
||||
// import 'package:thermion_dart/thermion_dart.dart';
|
||||
// import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
class DefaultPickDelegate extends PickDelegate {
|
||||
final ThermionViewer viewer;
|
||||
// class DefaultPickDelegate extends PickDelegate {
|
||||
// final ThermionViewer viewer;
|
||||
|
||||
DefaultPickDelegate(this.viewer);
|
||||
// DefaultPickDelegate(this.viewer);
|
||||
|
||||
final _picked = StreamController<ThermionEntity>();
|
||||
Stream<ThermionEntity> get picked => _picked.stream;
|
||||
// final _picked = StreamController<ThermionEntity>();
|
||||
// Stream<ThermionEntity> get picked => _picked.stream;
|
||||
|
||||
Future dispose() async {
|
||||
_picked.close();
|
||||
}
|
||||
// Future dispose() async {
|
||||
// _picked.close();
|
||||
// }
|
||||
|
||||
@override
|
||||
void pick(Vector2 location) {
|
||||
viewer.view.pick(location.x.toInt(), location.y.toInt(), (result) {
|
||||
_picked.sink.add(result.entity);
|
||||
});
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// void pick(Vector2 location) {
|
||||
// viewer.view.pick(location.x.toInt(), location.y.toInt(), (result) {
|
||||
// _picked.sink.add(result.entity);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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 'package:vector_math/vector_math_64.dart';
|
||||
// import 'dart:async';
|
||||
// import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import '../../../viewer/viewer.dart';
|
||||
import '../../input.dart';
|
||||
// import '../../../viewer/viewer.dart';
|
||||
// import '../../input.dart';
|
||||
|
||||
///
|
||||
/// An [InputHandlerDelegate] that orbits the camera around a fixed
|
||||
/// point.
|
||||
///
|
||||
class FixedOrbitRotateInputHandlerDelegate implements InputHandlerDelegate {
|
||||
final View view;
|
||||
final double minimumDistance;
|
||||
late final Vector3 target;
|
||||
// ///
|
||||
// /// An [InputHandlerDelegate] that orbits the camera around a fixed
|
||||
// /// point.
|
||||
// ///
|
||||
// class FixedOrbitRotateInputHandlerDelegate implements InputHandlerDelegate {
|
||||
// final View view;
|
||||
// final double minimumDistance;
|
||||
// late final Vector3 target;
|
||||
|
||||
final double rotationSensitivity;
|
||||
final double zoomSensitivity;
|
||||
// final double rotationSensitivity;
|
||||
// final double zoomSensitivity;
|
||||
|
||||
Vector2 _queuedRotationDelta = Vector2.zero();
|
||||
double _queuedZoomDelta = 0.0;
|
||||
// Vector2 _queuedRotationDelta = Vector2.zero();
|
||||
// double _queuedZoomDelta = 0.0;
|
||||
|
||||
Timer? _updateTimer;
|
||||
// Timer? _updateTimer;
|
||||
|
||||
FixedOrbitRotateInputHandlerDelegate(
|
||||
this.view, {
|
||||
Vector3? target,
|
||||
this.minimumDistance = 10.0,
|
||||
this.rotationSensitivity = 0.01,
|
||||
this.zoomSensitivity = 0.1,
|
||||
}) {
|
||||
this.target = target ?? Vector3.zero();
|
||||
// FixedOrbitRotateInputHandlerDelegate(
|
||||
// this.view, {
|
||||
// Vector3? target,
|
||||
// this.minimumDistance = 10.0,
|
||||
// this.rotationSensitivity = 0.01,
|
||||
// this.zoomSensitivity = 0.1,
|
||||
// }) {
|
||||
// this.target = target ?? Vector3.zero();
|
||||
|
||||
view.getCamera().then((camera) {
|
||||
camera.lookAt(Vector3(0.0, 0, -minimumDistance),
|
||||
focus: this.target, up: Vector3(0.0, 1.0, 0.0));
|
||||
});
|
||||
}
|
||||
// view.getCamera().then((camera) {
|
||||
// camera.lookAt(Vector3(0.0, 0, -minimumDistance),
|
||||
// focus: this.target, up: Vector3(0.0, 1.0, 0.0));
|
||||
// });
|
||||
// }
|
||||
|
||||
void dispose() {
|
||||
_updateTimer?.cancel();
|
||||
}
|
||||
// void dispose() {
|
||||
// _updateTimer?.cancel();
|
||||
// }
|
||||
|
||||
@override
|
||||
Future<void> queue(InputAction action, Vector3? delta) async {
|
||||
if (delta == null) return;
|
||||
// @override
|
||||
// Future<void> queue(InputAction action, Vector3? delta) async {
|
||||
// if (delta == null) return;
|
||||
|
||||
switch (action) {
|
||||
case InputAction.ROTATE:
|
||||
_queuedRotationDelta += Vector2(delta.x, delta.y);
|
||||
break;
|
||||
case InputAction.TRANSLATE:
|
||||
_queuedZoomDelta += delta.z;
|
||||
break;
|
||||
case InputAction.PICK:
|
||||
break;
|
||||
case InputAction.NONE:
|
||||
// Do nothing
|
||||
break;
|
||||
case InputAction.ZOOM:
|
||||
_queuedZoomDelta += delta.z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// switch (action) {
|
||||
// case InputAction.ROTATE:
|
||||
// _queuedRotationDelta += Vector2(delta.x, delta.y);
|
||||
// break;
|
||||
// case InputAction.TRANSLATE:
|
||||
// _queuedZoomDelta += delta.z;
|
||||
// break;
|
||||
// case InputAction.PICK:
|
||||
// break;
|
||||
// case InputAction.NONE:
|
||||
// // Do nothing
|
||||
// break;
|
||||
// case InputAction.ZOOM:
|
||||
// _queuedZoomDelta -= (delta.z - 1.0);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
bool _executing = false;
|
||||
// bool _executing = false;
|
||||
|
||||
@override
|
||||
Future<Matrix4?> execute() async {
|
||||
if (_queuedRotationDelta.length2 == 0.0 && _queuedZoomDelta == 0.0) {
|
||||
return null;
|
||||
}
|
||||
// @override
|
||||
// Future<Matrix4?> execute() async {
|
||||
// if (_queuedRotationDelta.length2 == 0.0 && _queuedZoomDelta == 0.0) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
if (_executing) {
|
||||
return null;
|
||||
}
|
||||
// if (_executing) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
_executing = true;
|
||||
// _executing = true;
|
||||
|
||||
final camera = await view.getCamera();
|
||||
// final camera = await view.getCamera();
|
||||
|
||||
var modelMatrix = await camera.getModelMatrix();
|
||||
|
||||
Vector3 currentPosition = modelMatrix.getTranslation();
|
||||
// var modelMatrix = await camera.getModelMatrix();
|
||||
|
||||
Vector3 forward = modelMatrix.forward;
|
||||
// Vector3 currentPosition = modelMatrix.getTranslation();
|
||||
|
||||
if (forward.length == 0) {
|
||||
forward = Vector3(0, 0, -1);
|
||||
currentPosition = Vector3(0, 0, minimumDistance);
|
||||
}
|
||||
// Vector3 forward = modelMatrix.forward;
|
||||
|
||||
Matrix4? updatedModelMatrix = null;
|
||||
// if (forward.length == 0) {
|
||||
// forward = Vector3(0, 0, -1);
|
||||
// currentPosition = Vector3(0, 0, minimumDistance);
|
||||
// }
|
||||
|
||||
// Zoom
|
||||
if (_queuedZoomDelta != 0.0) {
|
||||
var newPosition = currentPosition +
|
||||
(currentPosition - target).scaled(_queuedZoomDelta * zoomSensitivity);
|
||||
// Matrix4? updatedModelMatrix = null;
|
||||
|
||||
var distToTarget = (newPosition - target).length;
|
||||
// // Zoom
|
||||
// if (_queuedZoomDelta != 0.0) {
|
||||
// print("_queuedZoomDelta $_queuedZoomDelta");
|
||||
// var newPosition = currentPosition +
|
||||
// (currentPosition - target).scaled(_queuedZoomDelta * zoomSensitivity);
|
||||
|
||||
// if we somehow overshot the minimum distance, reset the camera to the minimum distance
|
||||
if (distToTarget >= minimumDistance) {
|
||||
currentPosition = newPosition;
|
||||
// Calculate view matrix
|
||||
forward = (currentPosition - target).normalized();
|
||||
var right = modelMatrix.up.cross(forward).normalized();
|
||||
var up = forward.cross(right);
|
||||
// var distToTarget = (newPosition - target).length;
|
||||
|
||||
Matrix4 newViewMatrix = makeViewMatrix(currentPosition, target, up);
|
||||
newViewMatrix.invert();
|
||||
// // if we somehow overshot the minimum distance, reset the camera to the minimum distance
|
||||
// if (distToTarget >= minimumDistance) {
|
||||
// currentPosition = newPosition;
|
||||
// // Calculate view matrix
|
||||
// forward = (currentPosition - target).normalized();
|
||||
// var right = modelMatrix.up.cross(forward).normalized();
|
||||
// var up = forward.cross(right);
|
||||
|
||||
await camera.setModelMatrix(newViewMatrix);
|
||||
updatedModelMatrix = newViewMatrix;
|
||||
}
|
||||
} else if (_queuedRotationDelta.length != 0) {
|
||||
double rotateX = _queuedRotationDelta.x * rotationSensitivity;
|
||||
double rotateY = _queuedRotationDelta.y * rotationSensitivity;
|
||||
// Matrix4 newViewMatrix = makeViewMatrix(currentPosition, target, up);
|
||||
// newViewMatrix.invert();
|
||||
|
||||
var modelMatrix = await camera.getModelMatrix();
|
||||
// await camera.setModelMatrix(newViewMatrix);
|
||||
// updatedModelMatrix = newViewMatrix;
|
||||
// }
|
||||
// } else if (_queuedRotationDelta.length != 0) {
|
||||
// double rotateX = _queuedRotationDelta.x * rotationSensitivity;
|
||||
// double rotateY = _queuedRotationDelta.y * rotationSensitivity;
|
||||
|
||||
// for simplicity, we always assume a fixed coordinate system where
|
||||
// we are rotating around world Y and camera X
|
||||
var rot1 = Matrix4.identity()
|
||||
..setRotation(Quaternion.axisAngle(Vector3(0, 1, 0), -rotateX)
|
||||
.asRotationMatrix());
|
||||
var rot2 = Matrix4.identity()
|
||||
..setRotation(Quaternion.axisAngle(modelMatrix.right, rotateY)
|
||||
.asRotationMatrix());
|
||||
// var modelMatrix = await camera.getModelMatrix();
|
||||
|
||||
modelMatrix = rot1 * rot2 * modelMatrix;
|
||||
await camera.setModelMatrix(modelMatrix);
|
||||
updatedModelMatrix = modelMatrix;
|
||||
}
|
||||
// // for simplicity, we always assume a fixed coordinate system where
|
||||
// // we are rotating around world Y and camera X
|
||||
// var rot1 = Matrix4.identity()
|
||||
// ..setRotation(Quaternion.axisAngle(Vector3(0, 1, 0), -rotateX)
|
||||
// .asRotationMatrix());
|
||||
// var rot2 = Matrix4.identity()
|
||||
// ..setRotation(Quaternion.axisAngle(modelMatrix.right, rotateY)
|
||||
// .asRotationMatrix());
|
||||
|
||||
// Reset queued deltas
|
||||
_queuedRotationDelta = Vector2.zero();
|
||||
_queuedZoomDelta = 0.0;
|
||||
// modelMatrix = rot1 * rot2 * modelMatrix;
|
||||
// await camera.setModelMatrix(modelMatrix);
|
||||
// updatedModelMatrix = modelMatrix;
|
||||
// }
|
||||
|
||||
_executing = false;
|
||||
return updatedModelMatrix;
|
||||
}
|
||||
}
|
||||
// // Reset queued deltas
|
||||
// _queuedRotationDelta = Vector2.zero();
|
||||
// _queuedZoomDelta = 0.0;
|
||||
|
||||
// _executing = false;
|
||||
// return updatedModelMatrix;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,133 +1,133 @@
|
||||
import 'dart:async';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
import '../../../viewer/viewer.dart';
|
||||
import '../delegates.dart';
|
||||
import '../input_handler.dart';
|
||||
// import 'dart:async';
|
||||
// import 'package:vector_math/vector_math_64.dart';
|
||||
// import '../../../viewer/viewer.dart';
|
||||
// import '../../input.dart';
|
||||
|
||||
class FreeFlightInputHandlerDelegate implements InputHandlerDelegate {
|
||||
final View view;
|
||||
// class FreeFlightInputHandlerDelegate implements InputHandlerDelegate {
|
||||
// final View view;
|
||||
|
||||
final Vector3? minBounds;
|
||||
final Vector3? maxBounds;
|
||||
final double rotationSensitivity;
|
||||
final double movementSensitivity;
|
||||
final double zoomSensitivity;
|
||||
final double panSensitivity;
|
||||
final double? clampY;
|
||||
// final Vector3? minBounds;
|
||||
// final Vector3? maxBounds;
|
||||
// final double rotationSensitivity;
|
||||
// final double movementSensitivity;
|
||||
// final double zoomSensitivity;
|
||||
// final double panSensitivity;
|
||||
// final double? clampY;
|
||||
|
||||
Vector2 _queuedRotationDelta = Vector2.zero();
|
||||
Vector3 _queuedTranslateDelta = Vector3.zero();
|
||||
double _queuedZoomDelta = 0.0;
|
||||
Vector3 _queuedMoveDelta = Vector3.zero();
|
||||
// Vector2 _queuedRotationDelta = Vector2.zero();
|
||||
// Vector3 _queuedTranslateDelta = Vector3.zero();
|
||||
// double _queuedZoomDelta = 0.0;
|
||||
// Vector3 _queuedMoveDelta = Vector3.zero();
|
||||
|
||||
FreeFlightInputHandlerDelegate(this.view,
|
||||
{this.minBounds,
|
||||
this.maxBounds,
|
||||
this.rotationSensitivity = 0.001,
|
||||
this.movementSensitivity = 0.1,
|
||||
this.zoomSensitivity = 0.1,
|
||||
this.panSensitivity = 0.1,
|
||||
this.clampY}) {}
|
||||
// FreeFlightInputHandlerDelegate(this.view,
|
||||
// {this.minBounds,
|
||||
// this.maxBounds,
|
||||
// this.rotationSensitivity = 0.001,
|
||||
// this.movementSensitivity = 0.1,
|
||||
// this.zoomSensitivity = 0.1,
|
||||
// this.panSensitivity = 0.1,
|
||||
// this.clampY}) {}
|
||||
|
||||
@override
|
||||
Future<void> queue(InputAction action, Vector3? delta) async {
|
||||
if (delta == null) return;
|
||||
// @override
|
||||
// Future<void> queue(InputAction action, Vector3? delta) async {
|
||||
// if (delta == null) return;
|
||||
|
||||
switch (action) {
|
||||
case InputAction.ROTATE:
|
||||
_queuedRotationDelta += Vector2(delta.x, delta.y);
|
||||
break;
|
||||
case InputAction.TRANSLATE:
|
||||
_queuedTranslateDelta += delta;
|
||||
break;
|
||||
case InputAction.PICK:
|
||||
_queuedZoomDelta += delta.z;
|
||||
break;
|
||||
case InputAction.NONE:
|
||||
break;
|
||||
case InputAction.ZOOM:
|
||||
_queuedZoomDelta += delta.z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// switch (action) {
|
||||
// case InputAction.ROTATE:
|
||||
// _queuedRotationDelta += Vector2(delta.x, delta.y);
|
||||
// break;
|
||||
// case InputAction.TRANSLATE:
|
||||
// _queuedTranslateDelta += delta;
|
||||
// break;
|
||||
// case InputAction.PICK:
|
||||
// _queuedZoomDelta += delta.z;
|
||||
// break;
|
||||
// case InputAction.NONE:
|
||||
// break;
|
||||
// case InputAction.ZOOM:
|
||||
// _queuedZoomDelta += delta.z;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
bool _executing = false;
|
||||
// bool _executing = false;
|
||||
|
||||
@override
|
||||
Future<Matrix4?> execute() async {
|
||||
if (_executing) {
|
||||
return null;
|
||||
}
|
||||
// @override
|
||||
// Future<Matrix4?> execute() async {
|
||||
// if (_executing) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
_executing = true;
|
||||
// _executing = true;
|
||||
|
||||
if (_queuedRotationDelta.length2 == 0.0 &&
|
||||
_queuedTranslateDelta.length2 == 0.0 &&
|
||||
_queuedZoomDelta == 0.0 &&
|
||||
_queuedMoveDelta.length2 == 0.0) {
|
||||
_executing = false;
|
||||
return null;
|
||||
}
|
||||
// if (_queuedRotationDelta.length2 == 0.0 &&
|
||||
// _queuedTranslateDelta.length2 == 0.0 &&
|
||||
// _queuedZoomDelta == 0.0 &&
|
||||
// _queuedMoveDelta.length2 == 0.0) {
|
||||
// _executing = false;
|
||||
// return null;
|
||||
// }
|
||||
|
||||
final activeCamera = await view.getCamera();
|
||||
// final activeCamera = await view.getCamera();
|
||||
|
||||
Matrix4 current = await activeCamera.getModelMatrix();
|
||||
// Matrix4 current = await activeCamera.getModelMatrix();
|
||||
|
||||
Vector3 relativeTranslation = Vector3.zero();
|
||||
Quaternion relativeRotation = Quaternion.identity();
|
||||
// Vector3 relativeTranslation = Vector3.zero();
|
||||
// Quaternion relativeRotation = Quaternion.identity();
|
||||
|
||||
if (_queuedRotationDelta.length2 > 0.0) {
|
||||
double deltaX = _queuedRotationDelta.x * rotationSensitivity;
|
||||
double deltaY = _queuedRotationDelta.y * rotationSensitivity;
|
||||
relativeRotation = Quaternion.axisAngle(current.up, -deltaX) *
|
||||
Quaternion.axisAngle(current.right, -deltaY);
|
||||
_queuedRotationDelta = Vector2.zero();
|
||||
}
|
||||
// if (_queuedRotationDelta.length2 > 0.0) {
|
||||
// double deltaX = _queuedRotationDelta.x * rotationSensitivity;
|
||||
// double deltaY = _queuedRotationDelta.y * rotationSensitivity;
|
||||
// relativeRotation = Quaternion.axisAngle(current.up, -deltaX) *
|
||||
// Quaternion.axisAngle(current.right, -deltaY);
|
||||
// _queuedRotationDelta = Vector2.zero();
|
||||
// }
|
||||
|
||||
// Apply (mouse) pan
|
||||
if (_queuedTranslateDelta.length2 > 0.0) {
|
||||
double deltaX = -_queuedTranslateDelta.x * panSensitivity;
|
||||
double deltaY = _queuedTranslateDelta.y * panSensitivity;
|
||||
double deltaZ = -_queuedTranslateDelta.z * panSensitivity;
|
||||
// // Apply (mouse) pan
|
||||
// if (_queuedTranslateDelta.length2 > 0.0) {
|
||||
// double deltaX = -_queuedTranslateDelta.x * panSensitivity;
|
||||
// double deltaY = _queuedTranslateDelta.y * panSensitivity;
|
||||
// double deltaZ = -_queuedTranslateDelta.z * panSensitivity;
|
||||
|
||||
relativeTranslation += current.right * deltaX +
|
||||
current.up * deltaY +
|
||||
current.forward * deltaZ;
|
||||
_queuedTranslateDelta = Vector3.zero();
|
||||
}
|
||||
// relativeTranslation += current.right * deltaX +
|
||||
// current.up * deltaY +
|
||||
// current.forward * deltaZ;
|
||||
// _queuedTranslateDelta = Vector3.zero();
|
||||
// }
|
||||
|
||||
// Apply zoom
|
||||
if (_queuedZoomDelta != 0.0) {
|
||||
var zoomTranslation = current.forward..scaled(zoomSensitivity);
|
||||
zoomTranslation.scale(_queuedZoomDelta);
|
||||
relativeTranslation += zoomTranslation;
|
||||
_queuedZoomDelta = 0.0;
|
||||
}
|
||||
// // Apply zoom
|
||||
// if (_queuedZoomDelta != 0.0) {
|
||||
// print("_queuedZoomDelta $_queuedZoomDelta");
|
||||
// var zoomTranslation = current.forward..scaled(zoomSensitivity);
|
||||
// zoomTranslation.scale(_queuedZoomDelta);
|
||||
// relativeTranslation += zoomTranslation;
|
||||
// _queuedZoomDelta = 0.0;
|
||||
// }
|
||||
|
||||
// Apply queued movement
|
||||
if (_queuedMoveDelta.length2 > 0.0) {
|
||||
relativeTranslation += (current.right * _queuedMoveDelta.x +
|
||||
current.up * _queuedMoveDelta.y +
|
||||
current.forward * _queuedMoveDelta.z) *
|
||||
movementSensitivity;
|
||||
// // Apply queued movement
|
||||
// if (_queuedMoveDelta.length2 > 0.0) {
|
||||
// relativeTranslation += (current.right * _queuedMoveDelta.x +
|
||||
// current.up * _queuedMoveDelta.y +
|
||||
// current.forward * _queuedMoveDelta.z) *
|
||||
// movementSensitivity;
|
||||
|
||||
_queuedMoveDelta = Vector3.zero();
|
||||
}
|
||||
// _queuedMoveDelta = Vector3.zero();
|
||||
// }
|
||||
|
||||
// // If the managed entity is not the active camera, we need to apply the rotation from the current camera model matrix
|
||||
// // to the entity's translation
|
||||
// if (await entity != activeCamera.getEntity()) {
|
||||
// Matrix4 modelMatrix = await activeCamera.getModelMatrix();
|
||||
// relativeTranslation = modelMatrix.getRotation() * relativeTranslation;
|
||||
// }
|
||||
// // // If the managed entity is not the active camera, we need to apply the rotation from the current camera model matrix
|
||||
// // // to the entity's translation
|
||||
// // if (await entity != activeCamera.getEntity()) {
|
||||
// // Matrix4 modelMatrix = await activeCamera.getModelMatrix();
|
||||
// // relativeTranslation = modelMatrix.getRotation() * relativeTranslation;
|
||||
// // }
|
||||
|
||||
var updated = Matrix4.compose(
|
||||
relativeTranslation, relativeRotation, Vector3(1, 1, 1)) *
|
||||
current;
|
||||
|
||||
await activeCamera.setModelMatrix(updated);
|
||||
// var updated = Matrix4.compose(
|
||||
// relativeTranslation, relativeRotation, Vector3(1, 1, 1)) *
|
||||
// current;
|
||||
|
||||
_executing = false;
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
// await activeCamera.setModelMatrix(updated);
|
||||
|
||||
// _executing = false;
|
||||
// return updated;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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:math';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
|
||||
class _Gizmo {
|
||||
final ThermionViewer viewer;
|
||||
|
||||
final GizmoAsset _gizmo;
|
||||
|
||||
final transformUpdates = StreamController<({Matrix4 transform})>.broadcast();
|
||||
|
||||
Axis? _active;
|
||||
final GizmoType type;
|
||||
|
||||
_Gizmo(this._gizmo, this.viewer, this.type);
|
||||
|
||||
static Future<_Gizmo> forType(ThermionViewer viewer, GizmoType type) async {
|
||||
final view = await viewer.view;
|
||||
return _Gizmo(await viewer.getGizmo(type), viewer, type);
|
||||
}
|
||||
|
||||
Future dispose() async {
|
||||
await transformUpdates.close();
|
||||
await viewer.destroyAsset(_gizmo);
|
||||
}
|
||||
|
||||
Future hide() async {
|
||||
final scene = await viewer.view.getScene();
|
||||
await scene.remove(_gizmo);
|
||||
}
|
||||
|
||||
Future reveal() async {
|
||||
final scene = await viewer.view.getScene();
|
||||
await scene.add(_gizmo);
|
||||
gizmoTransform = await _gizmo.getWorldTransform();
|
||||
}
|
||||
|
||||
double _getAngleBetweenVectors(Vector2 v1, Vector2 v2) {
|
||||
// Normalize vectors to ensure consistent rotation regardless of distance from center
|
||||
v1.normalize();
|
||||
v2.normalize();
|
||||
|
||||
// Calculate angle using atan2
|
||||
double angle = atan2(v2.y, v2.x) - atan2(v1.y, v1.x);
|
||||
|
||||
// Ensure angle is between -π and π
|
||||
if (angle > pi) angle -= 2 * pi;
|
||||
if (angle < -pi) angle += 2 * pi;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
void checkHover(int x, int y) async {
|
||||
_gizmo.pick(x, y, handler: (result, coords) async {
|
||||
switch (result) {
|
||||
case GizmoPickResultType.None:
|
||||
await _gizmo.unhighlight();
|
||||
_active = null;
|
||||
break;
|
||||
case GizmoPickResultType.AxisX:
|
||||
_active = Axis.X;
|
||||
case GizmoPickResultType.AxisY:
|
||||
_active = Axis.Y;
|
||||
case GizmoPickResultType.AxisZ:
|
||||
_active = Axis.Z;
|
||||
default:
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Matrix4? gizmoTransform;
|
||||
|
||||
void _updateTransform(Vector2 currentPosition, Vector2 delta) async {
|
||||
if (type == GizmoType.translation) {
|
||||
await _updateTranslation(currentPosition, delta);
|
||||
} else if (type == GizmoType.rotation) {
|
||||
await _updateRotation(currentPosition, delta);
|
||||
}
|
||||
|
||||
await _gizmo.setTransform(gizmoTransform!);
|
||||
|
||||
transformUpdates.add((transform: gizmoTransform!));
|
||||
}
|
||||
|
||||
Future<void>? _updateTranslation(
|
||||
Vector2 currentPosition, Vector2 delta) async {
|
||||
var view = await viewer.view;
|
||||
var camera = await viewer.getActiveCamera();
|
||||
var viewport = await view.getViewport();
|
||||
var projectionMatrix = await camera.getProjectionMatrix();
|
||||
var viewMatrix = await camera.getViewMatrix();
|
||||
var inverseViewMatrix = await camera.getModelMatrix();
|
||||
var inverseProjectionMatrix = projectionMatrix.clone()..invert();
|
||||
|
||||
// get gizmo position in screenspace
|
||||
var gizmoPositionWorldSpace = gizmoTransform!.getTranslation();
|
||||
Vector4 gizmoClipSpace = projectionMatrix *
|
||||
viewMatrix *
|
||||
Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y,
|
||||
gizmoPositionWorldSpace.z, 1.0);
|
||||
|
||||
var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w;
|
||||
|
||||
var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width,
|
||||
viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height));
|
||||
|
||||
gizmoScreenSpace += delta;
|
||||
|
||||
gizmoNdc = Vector4(((gizmoScreenSpace.x / viewport.width) - 0.5) * 2,
|
||||
(((gizmoScreenSpace.y / viewport.height)) - 0.5) * -2, gizmoNdc.z, 1.0);
|
||||
|
||||
var gizmoViewSpace = inverseProjectionMatrix * gizmoNdc;
|
||||
gizmoViewSpace /= gizmoViewSpace.w;
|
||||
|
||||
var newPosition = (inverseViewMatrix * gizmoViewSpace).xyz;
|
||||
|
||||
Vector3 worldSpaceDelta = newPosition - gizmoTransform!.getTranslation();
|
||||
worldSpaceDelta.multiply(_active!.asVector());
|
||||
|
||||
gizmoTransform!
|
||||
.setTranslation(gizmoTransform!.getTranslation() + worldSpaceDelta);
|
||||
}
|
||||
|
||||
Future<void>? _updateRotation(Vector2 currentPosition, Vector2 delta) async {
|
||||
var camera = await viewer.view.getCamera();
|
||||
var viewport = await viewer.view.getViewport();
|
||||
var projectionMatrix = await camera.getProjectionMatrix();
|
||||
var viewMatrix = await camera.getViewMatrix();
|
||||
|
||||
// Get gizmo center in screen space
|
||||
var gizmoPositionWorldSpace = gizmoTransform!.getTranslation();
|
||||
Vector4 gizmoClipSpace = projectionMatrix *
|
||||
viewMatrix *
|
||||
Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y,
|
||||
gizmoPositionWorldSpace.z, 1.0);
|
||||
|
||||
var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w;
|
||||
var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width,
|
||||
viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height));
|
||||
|
||||
// Calculate vectors from gizmo center to previous and current mouse positions
|
||||
var prevVector = (currentPosition - delta) - gizmoScreenSpace;
|
||||
var currentVector = currentPosition - gizmoScreenSpace;
|
||||
|
||||
// Calculate rotation angle based on the active axis
|
||||
double rotationAngle = 0.0;
|
||||
switch (_active) {
|
||||
case Axis.X:
|
||||
// For X axis, project onto YZ plane
|
||||
var prev = Vector2(prevVector.y, -prevVector.x);
|
||||
var curr = Vector2(currentVector.y, -currentVector.x);
|
||||
rotationAngle = _getAngleBetweenVectors(prev, curr);
|
||||
break;
|
||||
case Axis.Y:
|
||||
// For Y axis, project onto XZ plane
|
||||
var prev = Vector2(prevVector.x, -prevVector.y);
|
||||
var curr = Vector2(currentVector.x, -currentVector.y);
|
||||
rotationAngle = _getAngleBetweenVectors(prev, curr);
|
||||
break;
|
||||
case Axis.Z:
|
||||
// For Z axis, use screen plane directly
|
||||
rotationAngle = -1 * _getAngleBetweenVectors(prevVector, currentVector);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Create rotation matrix based on the active axis
|
||||
var rotationMatrix = Matrix4.identity();
|
||||
switch (_active) {
|
||||
case Axis.X:
|
||||
rotationMatrix.setRotationX(rotationAngle);
|
||||
break;
|
||||
case Axis.Y:
|
||||
rotationMatrix.setRotationY(rotationAngle);
|
||||
break;
|
||||
case Axis.Z:
|
||||
rotationMatrix.setRotationZ(rotationAngle);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply rotation to the current transform
|
||||
gizmoTransform = gizmoTransform! * rotationMatrix;
|
||||
}
|
||||
}
|
||||
|
||||
class GizmoInputHandler extends InputHandler {
|
||||
final ThermionViewer viewer;
|
||||
|
||||
late final _gizmos = <GizmoType, _Gizmo>{};
|
||||
|
||||
_Gizmo? _active;
|
||||
|
||||
ThermionEntity? _attached;
|
||||
|
||||
Future attach(ThermionEntity entity) async {
|
||||
if (_attached != null) {
|
||||
await detach();
|
||||
}
|
||||
_attached = entity;
|
||||
if (_active != null) {
|
||||
await FilamentApp.instance!.setParent(_attached!, _active!._gizmo.entity);
|
||||
await _active!.reveal();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Matrix4?> getGizmoTransform() async {
|
||||
return _active?.gizmoTransform;
|
||||
}
|
||||
|
||||
Future detach() async {
|
||||
if (_attached == null) {
|
||||
return;
|
||||
}
|
||||
await FilamentApp.instance!.setParent(_attached!, null);
|
||||
await _active?.hide();
|
||||
_attached = null;
|
||||
}
|
||||
|
||||
final _initialized = Completer<bool>();
|
||||
|
||||
final _transformController = StreamController<Matrix4>.broadcast();
|
||||
Stream<Matrix4> get transformUpdated => _transformController.stream;
|
||||
|
||||
final _pickResultController = StreamController<ThermionEntity?>.broadcast();
|
||||
Stream<ThermionEntity?> get onPickResult => _pickResultController.stream;
|
||||
|
||||
GizmoInputHandler({required this.viewer, required GizmoType initialType}) {
|
||||
initialize().then((_) {
|
||||
setGizmoType(initialType);
|
||||
});
|
||||
}
|
||||
|
||||
GizmoType? getGizmoType() {
|
||||
return _active?.type;
|
||||
}
|
||||
|
||||
Future setGizmoType(GizmoType? type) async {
|
||||
if (type == null) {
|
||||
await detach();
|
||||
_active?.hide();
|
||||
_active = null;
|
||||
} else {
|
||||
_active?.hide();
|
||||
_active = _gizmos[type]!;
|
||||
_active!.reveal();
|
||||
if (_attached != null) {
|
||||
await attach(_attached!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future initialize() async {
|
||||
if (_initialized.isCompleted) {
|
||||
throw Exception("Already initialized");
|
||||
}
|
||||
await viewer.initialized;
|
||||
|
||||
_gizmos[GizmoType.translation] =
|
||||
await _Gizmo.forType(viewer, GizmoType.translation);
|
||||
_gizmos[GizmoType.rotation] =
|
||||
await _Gizmo.forType(viewer, GizmoType.rotation);
|
||||
await setGizmoType(GizmoType.translation);
|
||||
for (final gizmo in _gizmos.values) {
|
||||
gizmo.transformUpdates.stream.listen((update) {
|
||||
_transformController.add(update.transform);
|
||||
});
|
||||
}
|
||||
_initialized.complete(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future dispose() async {
|
||||
_gizmos[GizmoType.rotation]!.dispose();
|
||||
_gizmos[GizmoType.translation]!.dispose();
|
||||
_gizmos.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
InputAction? getActionForType(InputType gestureType) {
|
||||
if (gestureType == InputType.LMB_DOWN) {
|
||||
return InputAction.PICK;
|
||||
}
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get initialized => _initialized.future;
|
||||
|
||||
@override
|
||||
void keyDown(PhysicalKey key) {}
|
||||
|
||||
@override
|
||||
void keyUp(PhysicalKey key) {}
|
||||
|
||||
@override
|
||||
Future<void>? onPointerDown(Vector2 localPosition, bool isMiddle) async {
|
||||
if (!_initialized.isCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMiddle) {
|
||||
return;
|
||||
}
|
||||
|
||||
await viewer.view.pick(localPosition.x.toInt(), localPosition.y.toInt(),
|
||||
(result) async {
|
||||
if (_active?._gizmo.isNonPickable(result.entity) == true ||
|
||||
result.entity == FILAMENT_ENTITY_NULL) {
|
||||
_pickResultController.add(null);
|
||||
return;
|
||||
}
|
||||
if (_active?._gizmo.isGizmoEntity(result.entity) != true) {
|
||||
_pickResultController.add(result.entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? onPointerHover(Vector2 localPosition, Vector2 delta) async {
|
||||
if (!_initialized.isCompleted) {
|
||||
return;
|
||||
}
|
||||
_active?.checkHover(localPosition.x.floor(), localPosition.y.floor());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? onPointerMove(
|
||||
Vector2 localPosition, Vector2 delta, bool isMiddle) async {
|
||||
if (!isMiddle && _active?._active != null) {
|
||||
final scaledDelta = Vector2(
|
||||
delta.x,
|
||||
delta.y,
|
||||
);
|
||||
_active!._updateTransform(localPosition, scaledDelta);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? onPointerScroll(
|
||||
Vector2 localPosition, double scrollDelta) async {}
|
||||
|
||||
@override
|
||||
Future<void>? onPointerUp(bool isMiddle) async {}
|
||||
|
||||
@override
|
||||
Future<void>? onScaleEnd(int pointerCount, double velocity) {}
|
||||
|
||||
@override
|
||||
Future<void>? onScaleStart(
|
||||
Vector2 focalPoint, int pointerCount, Duration? sourceTimestamp) {}
|
||||
|
||||
@override
|
||||
Future<void>? onScaleUpdate(
|
||||
Vector2 focalPoint,
|
||||
Vector2 focalPointDelta,
|
||||
double horizontalScale,
|
||||
double verticalScale,
|
||||
double scale,
|
||||
int pointerCount,
|
||||
double rotation,
|
||||
Duration? sourceTimestamp) {}
|
||||
|
||||
@override
|
||||
void setActionForType(InputType gestureType, InputAction gestureAction) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
// import 'dart:async';
|
||||
// import 'dart:math';
|
||||
// import 'package:thermion_dart/thermion_dart.dart';
|
||||
|
||||
// class _Gizmo {
|
||||
// final ThermionViewer viewer;
|
||||
|
||||
// final GizmoAsset _gizmo;
|
||||
|
||||
// final transformUpdates = StreamController<({Matrix4 transform})>.broadcast();
|
||||
|
||||
// Axis? _active;
|
||||
// final GizmoType type;
|
||||
|
||||
// _Gizmo(this._gizmo, this.viewer, this.type);
|
||||
|
||||
// static Future<_Gizmo> forType(ThermionViewer viewer, GizmoType type) async {
|
||||
// final view = await viewer.view;
|
||||
// return _Gizmo(await viewer.getGizmo(type), viewer, type);
|
||||
// }
|
||||
|
||||
// Future dispose() async {
|
||||
// await transformUpdates.close();
|
||||
// await viewer.destroyAsset(_gizmo);
|
||||
// }
|
||||
|
||||
// Future hide() async {
|
||||
// final scene = await viewer.view.getScene();
|
||||
// await scene.remove(_gizmo);
|
||||
// }
|
||||
|
||||
// Future reveal() async {
|
||||
// final scene = await viewer.view.getScene();
|
||||
// await scene.add(_gizmo);
|
||||
// gizmoTransform = await _gizmo.getWorldTransform();
|
||||
// }
|
||||
|
||||
// double _getAngleBetweenVectors(Vector2 v1, Vector2 v2) {
|
||||
// // Normalize vectors to ensure consistent rotation regardless of distance from center
|
||||
// v1.normalize();
|
||||
// v2.normalize();
|
||||
|
||||
// // Calculate angle using atan2
|
||||
// double angle = atan2(v2.y, v2.x) - atan2(v1.y, v1.x);
|
||||
|
||||
// // Ensure angle is between -π and π
|
||||
// if (angle > pi) angle -= 2 * pi;
|
||||
// if (angle < -pi) angle += 2 * pi;
|
||||
|
||||
// return angle;
|
||||
// }
|
||||
|
||||
// void checkHover(int x, int y) async {
|
||||
// _gizmo.pick(x, y, handler: (result, coords) async {
|
||||
// switch (result) {
|
||||
// case GizmoPickResultType.None:
|
||||
// await _gizmo.unhighlight();
|
||||
// _active = null;
|
||||
// break;
|
||||
// case GizmoPickResultType.AxisX:
|
||||
// _active = Axis.X;
|
||||
// case GizmoPickResultType.AxisY:
|
||||
// _active = Axis.Y;
|
||||
// case GizmoPickResultType.AxisZ:
|
||||
// _active = Axis.Z;
|
||||
// default:
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// Matrix4? gizmoTransform;
|
||||
|
||||
// void _updateTransform(Vector2 currentPosition, Vector2 delta) async {
|
||||
// if (type == GizmoType.translation) {
|
||||
// await _updateTranslation(currentPosition, delta);
|
||||
// } else if (type == GizmoType.rotation) {
|
||||
// await _updateRotation(currentPosition, delta);
|
||||
// }
|
||||
|
||||
// await _gizmo.setTransform(gizmoTransform!);
|
||||
|
||||
// transformUpdates.add((transform: gizmoTransform!));
|
||||
// }
|
||||
|
||||
// Future<void>? _updateTranslation(
|
||||
// Vector2 currentPosition, Vector2 delta) async {
|
||||
// var view = await viewer.view;
|
||||
// var camera = await viewer.getActiveCamera();
|
||||
// var viewport = await view.getViewport();
|
||||
// var projectionMatrix = await camera.getProjectionMatrix();
|
||||
// var viewMatrix = await camera.getViewMatrix();
|
||||
// var inverseViewMatrix = await camera.getModelMatrix();
|
||||
// var inverseProjectionMatrix = projectionMatrix.clone()..invert();
|
||||
|
||||
// // get gizmo position in screenspace
|
||||
// var gizmoPositionWorldSpace = gizmoTransform!.getTranslation();
|
||||
// Vector4 gizmoClipSpace = projectionMatrix *
|
||||
// viewMatrix *
|
||||
// Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y,
|
||||
// gizmoPositionWorldSpace.z, 1.0);
|
||||
|
||||
// var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w;
|
||||
|
||||
// var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width,
|
||||
// viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height));
|
||||
|
||||
// gizmoScreenSpace += delta;
|
||||
|
||||
// gizmoNdc = Vector4(((gizmoScreenSpace.x / viewport.width) - 0.5) * 2,
|
||||
// (((gizmoScreenSpace.y / viewport.height)) - 0.5) * -2, gizmoNdc.z, 1.0);
|
||||
|
||||
// var gizmoViewSpace = inverseProjectionMatrix * gizmoNdc;
|
||||
// gizmoViewSpace /= gizmoViewSpace.w;
|
||||
|
||||
// var newPosition = (inverseViewMatrix * gizmoViewSpace).xyz;
|
||||
|
||||
// Vector3 worldSpaceDelta = newPosition - gizmoTransform!.getTranslation();
|
||||
// worldSpaceDelta.multiply(_active!.asVector());
|
||||
|
||||
// gizmoTransform!
|
||||
// .setTranslation(gizmoTransform!.getTranslation() + worldSpaceDelta);
|
||||
// }
|
||||
|
||||
// Future<void>? _updateRotation(Vector2 currentPosition, Vector2 delta) async {
|
||||
// var camera = await viewer.view.getCamera();
|
||||
// var viewport = await viewer.view.getViewport();
|
||||
// var projectionMatrix = await camera.getProjectionMatrix();
|
||||
// var viewMatrix = await camera.getViewMatrix();
|
||||
|
||||
// // Get gizmo center in screen space
|
||||
// var gizmoPositionWorldSpace = gizmoTransform!.getTranslation();
|
||||
// Vector4 gizmoClipSpace = projectionMatrix *
|
||||
// viewMatrix *
|
||||
// Vector4(gizmoPositionWorldSpace.x, gizmoPositionWorldSpace.y,
|
||||
// gizmoPositionWorldSpace.z, 1.0);
|
||||
|
||||
// var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w;
|
||||
// var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width,
|
||||
// viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height));
|
||||
|
||||
// // Calculate vectors from gizmo center to previous and current mouse positions
|
||||
// var prevVector = (currentPosition - delta) - gizmoScreenSpace;
|
||||
// var currentVector = currentPosition - gizmoScreenSpace;
|
||||
|
||||
// // Calculate rotation angle based on the active axis
|
||||
// double rotationAngle = 0.0;
|
||||
// switch (_active) {
|
||||
// case Axis.X:
|
||||
// // For X axis, project onto YZ plane
|
||||
// var prev = Vector2(prevVector.y, -prevVector.x);
|
||||
// var curr = Vector2(currentVector.y, -currentVector.x);
|
||||
// rotationAngle = _getAngleBetweenVectors(prev, curr);
|
||||
// break;
|
||||
// case Axis.Y:
|
||||
// // For Y axis, project onto XZ plane
|
||||
// var prev = Vector2(prevVector.x, -prevVector.y);
|
||||
// var curr = Vector2(currentVector.x, -currentVector.y);
|
||||
// rotationAngle = _getAngleBetweenVectors(prev, curr);
|
||||
// break;
|
||||
// case Axis.Z:
|
||||
// // For Z axis, use screen plane directly
|
||||
// rotationAngle = -1 * _getAngleBetweenVectors(prevVector, currentVector);
|
||||
// break;
|
||||
// default:
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Create rotation matrix based on the active axis
|
||||
// var rotationMatrix = Matrix4.identity();
|
||||
// switch (_active) {
|
||||
// case Axis.X:
|
||||
// rotationMatrix.setRotationX(rotationAngle);
|
||||
// break;
|
||||
// case Axis.Y:
|
||||
// rotationMatrix.setRotationY(rotationAngle);
|
||||
// break;
|
||||
// case Axis.Z:
|
||||
// rotationMatrix.setRotationZ(rotationAngle);
|
||||
// break;
|
||||
// default:
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Apply rotation to the current transform
|
||||
// gizmoTransform = gizmoTransform! * rotationMatrix;
|
||||
// }
|
||||
// }
|
||||
|
||||
// class GizmoInputHandler extends InputHandler {
|
||||
// final ThermionViewer viewer;
|
||||
|
||||
// late final _gizmos = <GizmoType, _Gizmo>{};
|
||||
|
||||
// _Gizmo? _active;
|
||||
|
||||
// ThermionEntity? _attached;
|
||||
|
||||
// Future attach(ThermionEntity entity) async {
|
||||
// if (_attached != null) {
|
||||
// await detach();
|
||||
// }
|
||||
// _attached = entity;
|
||||
// if (_active != null) {
|
||||
// await FilamentApp.instance!.setParent(_attached!, _active!._gizmo.entity);
|
||||
// await _active!.reveal();
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<Matrix4?> getGizmoTransform() async {
|
||||
// return _active?.gizmoTransform;
|
||||
// }
|
||||
|
||||
// Future detach() async {
|
||||
// if (_attached == null) {
|
||||
// return;
|
||||
// }
|
||||
// await FilamentApp.instance!.setParent(_attached!, null);
|
||||
// await _active?.hide();
|
||||
// _attached = null;
|
||||
// }
|
||||
|
||||
// final _initialized = Completer<bool>();
|
||||
|
||||
// final _transformController = StreamController<Matrix4>.broadcast();
|
||||
// Stream<Matrix4> get transformUpdated => _transformController.stream;
|
||||
|
||||
// final _pickResultController = StreamController<ThermionEntity?>.broadcast();
|
||||
// Stream<ThermionEntity?> get onPickResult => _pickResultController.stream;
|
||||
|
||||
// GizmoInputHandler({required this.viewer, required GizmoType initialType}) {
|
||||
// initialize().then((_) {
|
||||
// setGizmoType(initialType);
|
||||
// });
|
||||
// }
|
||||
|
||||
// GizmoType? getGizmoType() {
|
||||
// return _active?.type;
|
||||
// }
|
||||
|
||||
// Future setGizmoType(GizmoType? type) async {
|
||||
// if (type == null) {
|
||||
// await detach();
|
||||
// _active?.hide();
|
||||
// _active = null;
|
||||
// } else {
|
||||
// _active?.hide();
|
||||
// _active = _gizmos[type]!;
|
||||
// _active!.reveal();
|
||||
// if (_attached != null) {
|
||||
// await attach(_attached!);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future initialize() async {
|
||||
// if (_initialized.isCompleted) {
|
||||
// throw Exception("Already initialized");
|
||||
// }
|
||||
// await viewer.initialized;
|
||||
|
||||
// _gizmos[GizmoType.translation] =
|
||||
// await _Gizmo.forType(viewer, GizmoType.translation);
|
||||
// _gizmos[GizmoType.rotation] =
|
||||
// await _Gizmo.forType(viewer, GizmoType.rotation);
|
||||
// await setGizmoType(GizmoType.translation);
|
||||
// for (final gizmo in _gizmos.values) {
|
||||
// gizmo.transformUpdates.stream.listen((update) {
|
||||
// _transformController.add(update.transform);
|
||||
// });
|
||||
// }
|
||||
// _initialized.complete(true);
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Future dispose() async {
|
||||
// _gizmos[GizmoType.rotation]!.dispose();
|
||||
// _gizmos[GizmoType.translation]!.dispose();
|
||||
// _gizmos.clear();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// InputAction? getActionForType(InputType gestureType) {
|
||||
// if (gestureType == InputType.LMB_DOWN) {
|
||||
// return InputAction.PICK;
|
||||
// }
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Future<bool> get initialized => _initialized.future;
|
||||
|
||||
// @override
|
||||
// void keyDown(PhysicalKey key) {}
|
||||
|
||||
// @override
|
||||
// void keyUp(PhysicalKey key) {}
|
||||
|
||||
// @override
|
||||
// Future<void>? onPointerDown(Vector2 localPosition, bool isMiddle) async {
|
||||
// if (!_initialized.isCompleted) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (isMiddle) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await viewer.view.pick(localPosition.x.toInt(), localPosition.y.toInt(),
|
||||
// (result) async {
|
||||
// if (_active?._gizmo.isNonPickable(result.entity) == true ||
|
||||
// result.entity == FILAMENT_ENTITY_NULL) {
|
||||
// _pickResultController.add(null);
|
||||
// return;
|
||||
// }
|
||||
// if (_active?._gizmo.isGizmoEntity(result.entity) != true) {
|
||||
// _pickResultController.add(result.entity);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Future<void>? onPointerHover(Vector2 localPosition, Vector2 delta) async {
|
||||
// if (!_initialized.isCompleted) {
|
||||
// return;
|
||||
// }
|
||||
// _active?.checkHover(localPosition.x.floor(), localPosition.y.floor());
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Future<void>? onPointerMove(
|
||||
// Vector2 localPosition, Vector2 delta, bool isMiddle) async {
|
||||
// if (!isMiddle && _active?._active != null) {
|
||||
// final scaledDelta = Vector2(
|
||||
// delta.x,
|
||||
// delta.y,
|
||||
// );
|
||||
// _active!._updateTransform(localPosition, scaledDelta);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Future<void>? onPointerScroll(
|
||||
// Vector2 localPosition, double scrollDelta) async {}
|
||||
|
||||
// @override
|
||||
// Future<void>? onPointerUp(bool isMiddle) async {}
|
||||
|
||||
// @override
|
||||
// Future<void>? onScaleEnd(int pointerCount, double velocity) {}
|
||||
|
||||
// @override
|
||||
// Future<void>? onScaleStart(
|
||||
// Vector2 focalPoint, int pointerCount, Duration? sourceTimestamp) {}
|
||||
|
||||
// @override
|
||||
// Future<void>? onScaleUpdate(
|
||||
// Vector2 focalPoint,
|
||||
// Vector2 focalPointDelta,
|
||||
// double horizontalScale,
|
||||
// double verticalScale,
|
||||
// double scale,
|
||||
// int pointerCount,
|
||||
// double rotation,
|
||||
// Duration? sourceTimestamp) {}
|
||||
|
||||
// @override
|
||||
// void setActionForType(InputType gestureType, InputAction gestureAction) {
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,121 +1,120 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
import '../../../viewer/viewer.dart';
|
||||
import '../delegates.dart';
|
||||
import '../input_handler.dart';
|
||||
// import 'dart:async';
|
||||
// import 'dart:math';
|
||||
// import 'package:vector_math/vector_math_64.dart';
|
||||
// import '../../../viewer/viewer.dart';
|
||||
// import '../../input.dart';
|
||||
|
||||
class OverTheShoulderCameraDelegate implements InputHandlerDelegate {
|
||||
final ThermionViewer viewer;
|
||||
// class OverTheShoulderCameraDelegate implements InputHandlerDelegate {
|
||||
// final ThermionViewer viewer;
|
||||
|
||||
late ThermionAsset player;
|
||||
late Camera camera;
|
||||
// late ThermionAsset player;
|
||||
// late Camera camera;
|
||||
|
||||
final double rotationSensitivity;
|
||||
final double movementSensitivity;
|
||||
final double zoomSensitivity;
|
||||
final double panSensitivity;
|
||||
final double? clampY;
|
||||
// final double rotationSensitivity;
|
||||
// final double movementSensitivity;
|
||||
// final double zoomSensitivity;
|
||||
// final double panSensitivity;
|
||||
// final double? clampY;
|
||||
|
||||
static final _up = Vector3(0, 1, 0);
|
||||
static final _forward = Vector3(0, 0, -1);
|
||||
static final Vector3 _right = Vector3(1, 0, 0);
|
||||
// static final _up = Vector3(0, 1, 0);
|
||||
// static final _forward = Vector3(0, 0, -1);
|
||||
// static final Vector3 _right = Vector3(1, 0, 0);
|
||||
|
||||
Vector2 _queuedRotationDelta = Vector2.zero();
|
||||
double _queuedZoomDelta = 0.0;
|
||||
Vector3 _queuedMoveDelta = Vector3.zero();
|
||||
// Vector2 _queuedRotationDelta = Vector2.zero();
|
||||
// double _queuedZoomDelta = 0.0;
|
||||
// Vector3 _queuedMoveDelta = Vector3.zero();
|
||||
|
||||
final cameraPosition = Vector3(-0.5, 2.5, -3);
|
||||
final cameraUp = Vector3(0, 1, 0);
|
||||
var cameraLookAt = Vector3(0, 0.5, 3);
|
||||
// final cameraPosition = Vector3(-0.5, 2.5, -3);
|
||||
// final cameraUp = Vector3(0, 1, 0);
|
||||
// var cameraLookAt = Vector3(0, 0.5, 3);
|
||||
|
||||
final void Function(Matrix4 transform)? onUpdate;
|
||||
// final void Function(Matrix4 transform)? onUpdate;
|
||||
|
||||
OverTheShoulderCameraDelegate(this.viewer, this.player, this.camera,
|
||||
{this.rotationSensitivity = 0.001,
|
||||
this.movementSensitivity = 0.1,
|
||||
this.zoomSensitivity = 0.1,
|
||||
this.panSensitivity = 0.1,
|
||||
this.clampY,
|
||||
ThermionEntity? entity,
|
||||
this.onUpdate}) {}
|
||||
// OverTheShoulderCameraDelegate(this.viewer, this.player, this.camera,
|
||||
// {this.rotationSensitivity = 0.001,
|
||||
// this.movementSensitivity = 0.1,
|
||||
// this.zoomSensitivity = 0.1,
|
||||
// this.panSensitivity = 0.1,
|
||||
// this.clampY,
|
||||
// ThermionEntity? entity,
|
||||
// this.onUpdate}) {}
|
||||
|
||||
@override
|
||||
Future<void> queue(InputAction action, Vector3? delta) async {
|
||||
if (delta == null) return;
|
||||
// @override
|
||||
// Future<void> queue(InputAction action, Vector3? delta) async {
|
||||
// if (delta == null) return;
|
||||
|
||||
switch (action) {
|
||||
case InputAction.ROTATE:
|
||||
_queuedRotationDelta += Vector2(delta.x, delta.y);
|
||||
break;
|
||||
case InputAction.TRANSLATE:
|
||||
_queuedMoveDelta += delta;
|
||||
break;
|
||||
case InputAction.PICK:
|
||||
_queuedZoomDelta += delta.z;
|
||||
break;
|
||||
case InputAction.NONE:
|
||||
break;
|
||||
case InputAction.ZOOM:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// switch (action) {
|
||||
// case InputAction.ROTATE:
|
||||
// _queuedRotationDelta += Vector2(delta.x, delta.y);
|
||||
// break;
|
||||
// case InputAction.TRANSLATE:
|
||||
// _queuedMoveDelta += delta;
|
||||
// break;
|
||||
// case InputAction.PICK:
|
||||
// _queuedZoomDelta += delta.z;
|
||||
// break;
|
||||
// case InputAction.NONE:
|
||||
// break;
|
||||
// case InputAction.ZOOM:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
static bool _executing = false;
|
||||
static bool get executing => _executing;
|
||||
// static bool _executing = false;
|
||||
// static bool get executing => _executing;
|
||||
|
||||
@override
|
||||
Future<Matrix4?> execute() async {
|
||||
if (_executing) {
|
||||
return null;
|
||||
}
|
||||
// @override
|
||||
// Future<Matrix4?> execute() async {
|
||||
// if (_executing) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
_executing = true;
|
||||
// _executing = true;
|
||||
|
||||
if (_queuedRotationDelta.length2 == 0.0 &&
|
||||
_queuedZoomDelta == 0.0 &&
|
||||
_queuedMoveDelta.length2 == 0.0) {
|
||||
_executing = false;
|
||||
return null;
|
||||
}
|
||||
// if (_queuedRotationDelta.length2 == 0.0 &&
|
||||
// _queuedZoomDelta == 0.0 &&
|
||||
// _queuedMoveDelta.length2 == 0.0) {
|
||||
// _executing = false;
|
||||
// return null;
|
||||
// }
|
||||
|
||||
Matrix4 currentPlayerTransform = await player.getWorldTransform();
|
||||
// Matrix4 currentPlayerTransform = await player.getWorldTransform();
|
||||
|
||||
// first we need to convert the move vector to player space
|
||||
var newTransform =
|
||||
Matrix4.translation(_queuedMoveDelta * movementSensitivity);
|
||||
// // first we need to convert the move vector to player space
|
||||
// var newTransform =
|
||||
// Matrix4.translation(_queuedMoveDelta * movementSensitivity);
|
||||
|
||||
_queuedMoveDelta = Vector3.zero();
|
||||
Matrix4 newPlayerTransform = newTransform * currentPlayerTransform;
|
||||
await player.setTransform(newPlayerTransform);
|
||||
// _queuedMoveDelta = Vector3.zero();
|
||||
// Matrix4 newPlayerTransform = newTransform * currentPlayerTransform;
|
||||
// await player.setTransform(newPlayerTransform);
|
||||
|
||||
if (_queuedZoomDelta != 0.0) {
|
||||
// Ignore zoom
|
||||
}
|
||||
// if (_queuedZoomDelta != 0.0) {
|
||||
// // Ignore zoom
|
||||
// }
|
||||
|
||||
var inverted = newPlayerTransform.clone()..invert();
|
||||
// var inverted = newPlayerTransform.clone()..invert();
|
||||
|
||||
// camera is always looking at -Z, whereas models generally face towards +Z
|
||||
if (_queuedRotationDelta.length2 > 0.0) {
|
||||
double deltaX = _queuedRotationDelta.x * rotationSensitivity;
|
||||
double deltaY = _queuedRotationDelta.y * rotationSensitivity;
|
||||
// // camera is always looking at -Z, whereas models generally face towards +Z
|
||||
// if (_queuedRotationDelta.length2 > 0.0) {
|
||||
// double deltaX = _queuedRotationDelta.x * rotationSensitivity;
|
||||
// double deltaY = _queuedRotationDelta.y * rotationSensitivity;
|
||||
|
||||
cameraLookAt = Matrix4.rotationY(-deltaX) *
|
||||
Matrix4.rotationX(-deltaY) *
|
||||
cameraLookAt;
|
||||
_queuedRotationDelta = Vector2.zero();
|
||||
}
|
||||
// cameraLookAt = Matrix4.rotationY(-deltaX) *
|
||||
// Matrix4.rotationX(-deltaY) *
|
||||
// cameraLookAt;
|
||||
// _queuedRotationDelta = Vector2.zero();
|
||||
// }
|
||||
|
||||
var newCameraViewMatrix =
|
||||
makeViewMatrix(cameraPosition, cameraLookAt, cameraUp);
|
||||
newCameraViewMatrix.invert();
|
||||
var newCameraTransform = newPlayerTransform * newCameraViewMatrix;
|
||||
await camera.setTransform(newCameraTransform);
|
||||
// var newCameraViewMatrix =
|
||||
// makeViewMatrix(cameraPosition, cameraLookAt, cameraUp);
|
||||
// newCameraViewMatrix.invert();
|
||||
// var newCameraTransform = newPlayerTransform * newCameraViewMatrix;
|
||||
// await camera.setTransform(newCameraTransform);
|
||||
|
||||
// await viewer.queueTransformUpdates(
|
||||
// [camera.getEntity(), player], [newCameraTransform, newPlayerTransform]);
|
||||
onUpdate?.call(newPlayerTransform);
|
||||
_executing = false;
|
||||
return newCameraTransform;
|
||||
}
|
||||
}
|
||||
// // await viewer.queueTransformUpdates(
|
||||
// // [camera.getEntity(), player], [newCameraTransform, newPlayerTransform]);
|
||||
// onUpdate?.call(newPlayerTransform);
|
||||
// _executing = false;
|
||||
// return newCameraTransform;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,64 +1,32 @@
|
||||
import 'dart:async';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
enum InputType {
|
||||
LMB_DOWN,
|
||||
LMB_HOLD_AND_MOVE,
|
||||
LMB_UP,
|
||||
LMB_HOVER,
|
||||
MMB_DOWN,
|
||||
MMB_HOLD_AND_MOVE,
|
||||
|
||||
MMB_UP,
|
||||
MMB_HOVER,
|
||||
SCALE1,
|
||||
SCALE2, // two fingers pinchin in/out
|
||||
SCALE2_ROTATE, // two fingers rotating in a circle
|
||||
SCALE2_MOVE, // two fingers sliding along a line
|
||||
SCROLLWHEEL,
|
||||
POINTER_MOVE,
|
||||
KEYDOWN_W,
|
||||
KEYDOWN_A,
|
||||
KEYDOWN_S,
|
||||
KEYDOWN_D,
|
||||
}
|
||||
|
||||
enum PhysicalKey { W, A, S, D }
|
||||
|
||||
enum InputAction { TRANSLATE, ROTATE, PICK, ZOOM, NONE }
|
||||
import 'input_types.dart';
|
||||
|
||||
///
|
||||
/// An interface for handling user device input events.
|
||||
///
|
||||
abstract class InputHandler {
|
||||
|
||||
@Deprecated("Use @transformUpdated instead")
|
||||
Stream get cameraUpdated => transformUpdated;
|
||||
///
|
||||
///
|
||||
///
|
||||
Future? handle(InputEvent event);
|
||||
|
||||
Stream<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();
|
||||
|
||||
void setActionForType(InputType gestureType, InputAction gestureAction);
|
||||
InputAction? getActionForType(InputType gestureType);
|
||||
|
||||
void keyDown(PhysicalKey key);
|
||||
void keyUp(PhysicalKey key);
|
||||
|
||||
}
|
||||
|
||||
class InputSensitivityOptions {
|
||||
final double touchSensitivity;
|
||||
final double touchScaleSensitivity;
|
||||
final double mouseSensitivity;
|
||||
final double keySensitivity;
|
||||
final double scrollWheelSensitivity;
|
||||
|
||||
const InputSensitivityOptions(
|
||||
{this.touchSensitivity = 0.001,
|
||||
this.touchScaleSensitivity = 2.0,
|
||||
this.mouseSensitivity = 0.001,
|
||||
this.scrollWheelSensitivity = 0.01,
|
||||
this.keySensitivity = 0.1});
|
||||
}
|
||||
|
||||
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