Skip to content

Commit

Permalink
fix!: Add DisplacementEvent to fix delta coordinate transformations f…
Browse files Browse the repository at this point in the history
…or drag events (#2871)

This adds `DisplacementEvent` to fix delta coordinate transformations
for drag events, to be used instead of `PositionEvent`.
Drag Events now expose the start and end position, as well as the delta,
correctly transformed by the camera and zoom.
This also ensures that drag events, once starts, do not get lost if the
drag update leaves the component bounds.



* if you are using `DragUpdateEvent` events, the `devicePosition`,
`canvasPosition`, `localPosition`, and `delta` are deprecated as they
are unclear.
* use `xStartPosition` to get the position at the start of the drag
event ("from")
* use `xEndPosition` to get the position at the end of the drag event
("to")
* if you want the delta, use `localDelta`. it now already considers the
camera zoom. no need to manually account for that
* now you keep receiving drag events for the same component even if the
mouse leaves the component (breaking)

---------

Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
  • Loading branch information
luanpotter and spydon committed Nov 30, 2023
1 parent 07ef46c commit 63994eb
Show file tree
Hide file tree
Showing 21 changed files with 510 additions and 194 deletions.
4 changes: 2 additions & 2 deletions doc/flame/examples/lib/drag_events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class DragTarget extends PositionComponent with DragCallbacks {

@override
void onDragUpdate(DragUpdateEvent event) {
_trails[event.pointerId]!.addPoint(event.localPosition);
_trails[event.pointerId]!.addPoint(event.localEndPosition);
}

@override
Expand Down Expand Up @@ -240,6 +240,6 @@ class Star extends PositionComponent with DragCallbacks {

@override
void onDragUpdate(DragUpdateEvent event) {
position += event.delta;
position += event.localDelta;
}
}
3 changes: 1 addition & 2 deletions doc/tutorials/klondike/app/lib/step4/components/card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,7 @@ class Card extends PositionComponent with DragCallbacks {
if (!_isDragging) {
return;
}
final cameraZoom = findGame()!.camera.viewfinder.zoom;
final delta = event.delta / cameraZoom;
final delta = event.localDelta;
position.add(delta);
attachedCards.forEach((card) => card.position.add(delta));
}
Expand Down
3 changes: 1 addition & 2 deletions doc/tutorials/klondike/app/lib/step5/components/card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,7 @@ class Card extends PositionComponent
if (!_isDragging) {
return;
}
final cameraZoom = findGame()!.camera.viewfinder.zoom;
final delta = event.delta / cameraZoom;
final delta = event.localDelta;
position.add(delta);
attachedCards.forEach((card) => card.position.add(delta));
}
Expand Down
24 changes: 5 additions & 19 deletions doc/tutorials/klondike/step4.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,19 +502,13 @@ the card, so that it is rendered above all others. Without this, the card would
During the drag, the `onDragUpdate` event will be called continuously. Using this callback we will
be updating the position of the card so that it follows the movement of the finger (or the mouse).
The `event` object passed to this callback contains the most recent coordinate of the point of
touch, and also the `delta` property -- which is the displacement vector since the previous call of
`onDragUpdate`. The only problem is that this delta is measured in screen pixels, whereas we want
it to be in game world units. The conversion between the two is given by the camera zoom level, so
we will add an extra method to determine the zoom level:
touch, and also the `localDelta` property -- which is the displacement vector since the previous
call of `onDragUpdate`, considering the camera zoom.

```dart
@override
void onDragUpdate(DragUpdateEvent event) {
final cameraZoom = (findGame()! as FlameGame)
.camera
.viewfinder
.zoom;
position += event.delta / cameraZoom;
position += event.delta;
}
```

Expand Down Expand Up @@ -606,11 +600,7 @@ to `false`:
if (!isDragged) {
return;
}
final cameraZoom = (findGame()! as FlameGame)
.camera
.viewfinder
.zoom;
position += event.delta / cameraZoom;
position += event.delta;
}
@override
Expand Down Expand Up @@ -940,11 +930,7 @@ the `onDragUpdate` method:
if (!isDragged) {
return;
}
final cameraZoom = (findGame()! as FlameGame)
.camera
.viewfinder
.zoom;
final delta = event.delta / cameraZoom;
final delta = event.delta;
position.add(delta);
attachedCards.forEach((card) => card.position.add(delta));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class DraggableBall extends Ball with DragCallbacks {

@override
void onDragUpdate(DragUpdateEvent event) {
body.applyLinearImpulse(event.delta * 1000);
body.applyLinearImpulse(event.localDelta * 1000);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class MouseJointWorld extends Forge2DWorld

@override
void onDragUpdate(DragUpdateEvent info) {
mouseJoint?.setTarget(info.localPosition);
mouseJoint?.setTarget(info.localEndPosition);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,7 @@ class DraggableBox extends Box with DragCallbacks {

@override
bool onDragUpdate(DragUpdateEvent info) {
final target = info.localPosition;
if (target.isNaN) {
return false;
}
final target = info.localEndPosition;
final mouseJointDef = MouseJointDef()
..maxForce = body.mass * 300
..dampingRatio = 0
Expand Down
3 changes: 1 addition & 2 deletions examples/lib/stories/input/drag_callbacks_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ class DraggableEmber extends Ember with DragCallbacks {

@override
void onDragUpdate(DragUpdateEvent event) {
event.continuePropagation = true;
return;
position += event.localDelta;
}
}
50 changes: 35 additions & 15 deletions packages/flame/lib/src/camera/camera_component.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/src/camera/behaviors/bounded_position_behavior.dart';
import 'package:flame/src/camera/behaviors/follow_behavior.dart';
Expand All @@ -6,9 +7,6 @@ import 'package:flame/src/camera/viewfinder.dart';
import 'package:flame/src/camera/viewport.dart';
import 'package:flame/src/camera/viewports/fixed_resolution_viewport.dart';
import 'package:flame/src/camera/viewports/max_viewport.dart';
import 'package:flame/src/camera/world.dart';
import 'package:flame/src/components/core/component.dart';
import 'package:flame/src/components/position_component.dart';
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/move_by_effect.dart';
import 'package:flame/src/effects/move_effect.dart';
Expand Down Expand Up @@ -223,22 +221,44 @@ class CameraComponent extends Component {
return viewport.localToGlobal(viewfinderPosition, output: output);
}

final _viewportPoint = Vector2.zero();

@override
Iterable<Component> componentsAtPoint(
Vector2 point, [
List<Vector2>? nestedPoints,
]) sync* {
final viewportPoint = viewport.globalToLocal(point, output: _viewportPoint);
yield* viewport.componentsAtPoint(viewportPoint, nestedPoints);
Iterable<Component> componentsAtLocation<T>(
T locationContext,
List<T>? nestedContexts,
T? Function(CoordinateTransform, T) transformContext,
bool Function(Component, T) checkContains,
) sync* {
final viewportPoint = transformContext(viewport, locationContext);
if (viewportPoint == null) {
return;
}

yield* viewport.componentsAtLocation(
viewportPoint,
nestedContexts,
transformContext,
checkContains,
);
if ((world?.isMounted ?? false) &&
currentCameras.length < maxCamerasDepth) {
if (viewport.containsLocalPoint(_viewportPoint)) {
if (checkContains(viewport, viewportPoint)) {
currentCameras.add(this);
final worldPoint = viewfinder.transform.globalToLocal(_viewportPoint);
yield* viewfinder.componentsAtPoint(worldPoint, nestedPoints);
yield* world!.componentsAtPoint(worldPoint, nestedPoints);
final worldPoint = transformContext(viewfinder, viewportPoint);
if (worldPoint == null) {
return;
}
yield* viewfinder.componentsAtLocation(
worldPoint,
nestedContexts,
transformContext,
checkContains,
);
yield* world!.componentsAtLocation(
worldPoint,
nestedContexts,
transformContext,
checkContains,
);
currentCameras.removeLast();
}
}
Expand Down
55 changes: 46 additions & 9 deletions packages/flame/lib/src/components/core/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ import 'package:meta/meta.dart';
/// [update]d, and then all the components will be [render]ed.
///
/// You may also need to override [containsLocalPoint] if the component needs to
/// respond to tap events or similar; the [componentsAtPoint] may also need to
/// be overridden if you have reimplemented [renderTree].
/// respond to tap events or similar; the [componentsAtLocation] may also need
/// to be overridden if you have reimplemented [renderTree].
class Component {
Component({
Iterable<Component>? children,
Expand Down Expand Up @@ -704,28 +704,65 @@ class Component {
Iterable<Component> componentsAtPoint(
Vector2 point, [
List<Vector2>? nestedPoints,
]) sync* {
nestedPoints?.add(point);
]) {
return componentsAtLocation<Vector2>(
point,
nestedPoints,
(transform, point) => transform.parentToLocal(point),
(component, point) => component.containsLocalPoint(point),
);
}

/// This is a generic implementation of [componentsAtPoint]; refer to those
/// docs for context.
///
/// This will find components intersecting a given location context [T]. The
/// context can be a single point or a more complicated structure. How to
/// interpret the structure T is determined by the provided lambdas,
/// [transformContext] and [checkContains].
///
/// A simple choice of T would be a simple point (i.e. Vector2). In that case
/// transformContext needs to be able to transform a Vector2 on the parent
/// coordinate space into the coordinate space of a provided
/// [CoordinateTransform]; and [checkContains] must be able to determine if
/// a given [Component] "contains" the Vector2 (the definition of "contains"
/// will vary and shall be determined by the nature of the chosen location
/// context [T]).
Iterable<Component> componentsAtLocation<T>(
T locationContext,
List<T>? nestedContexts,
T? Function(CoordinateTransform, T) transformContext,
bool Function(Component, T) checkContains,
) sync* {
nestedContexts?.add(locationContext);
if (_children != null) {
for (final child in _children!.reversed()) {
if (child is IgnoreEvents && child.ignoreEvents) {
continue;
}
Vector2? childPoint = point;
T? childPoint = locationContext;
if (child is CoordinateTransform) {
childPoint = (child as CoordinateTransform).parentToLocal(point);
childPoint = transformContext(
child as CoordinateTransform,
locationContext,
);
}
if (childPoint != null) {
yield* child.componentsAtPoint(childPoint, nestedPoints);
yield* child.componentsAtLocation(
childPoint,
nestedContexts,
transformContext,
checkContains,
);
}
}
}
final shouldIgnoreEvents =
this is IgnoreEvents && (this as IgnoreEvents).ignoreEvents;
if (containsLocalPoint(point) && !shouldIgnoreEvents) {
if (checkContains(this, locationContext) && !shouldIgnoreEvents) {
yield this;
}
nestedPoints?.removeLast();
nestedContexts?.removeLast();
}

//#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class JoystickComponent extends HudMarginComponent with DragCallbacks {

@override
bool onDragUpdate(DragUpdateEvent event) {
_unscaledDelta.add(event.delta);
_unscaledDelta.add(event.localDelta);
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import 'package:vector_math/vector_math_64.dart';
/// transformation, that is, the transform applies to all children of the
/// component equally. If that is not the case (for example, the component does
/// different transformations for some of its children), then that component
/// must implement [Component.componentsAtPoint] method instead.
/// must implement [Component.componentsAtLocation] method instead.
///
/// The two methods of this interface convert between the parent's coordinate
/// space and the local coordinates. The methods may also return `null`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:flame/src/components/core/component.dart';
/// This mixin allows a component and all it's descendants to ignore events.
///
/// Do note that this will also ignore the component and its descendants in
/// calls to [Component.componentsAtPoint].
/// calls to [Component.componentsAtLocation].
///
/// If you want to dynamically use this mixin, you can add it and set
/// [ignoreEvents] true or false at runtime.
Expand Down
22 changes: 13 additions & 9 deletions packages/flame/lib/src/components/route.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import 'dart:ui';

import 'package:flame/src/components/core/component.dart';
import 'package:flame/src/components/mixins/parent_is_a.dart';
import 'package:flame/src/components/position_component.dart';
import 'package:flame/components.dart';
import 'package:flame/src/components/router_component.dart';
import 'package:flame/src/effects/effect.dart';
import 'package:flame/src/rendering/decorator.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';

/// [Route] is a light-weight component that builds and manages a page.
///
Expand Down Expand Up @@ -166,12 +163,19 @@ class Route extends PositionComponent with ParentIsA<RouterComponent> {
}

@override
Iterable<Component> componentsAtPoint(
Vector2 point, [
List<Vector2>? nestedPoints,
]) {
Iterable<Component> componentsAtLocation<T>(
T locationContext,
List<T>? nestedContexts,
T? Function(CoordinateTransform, T) transformContext,
bool Function(Component, T) checkContains,
) {
if (isRendered) {
return super.componentsAtPoint(point, nestedPoints);
return super.componentsAtLocation(
locationContext,
nestedContexts,
transformContext,
checkContains,
);
} else {
return const Iterable<Component>.empty();
}
Expand Down
Loading

0 comments on commit 63994eb

Please sign in to comment.