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