Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement panning in FirstPersonController #8166

Merged
merged 9 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/api-reference/core/first-person-controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Inherits from [Base Controller](./controller.md).

The `FirstPersonController` class can be passed to either the `Deck` class's [controller](./deck.md#controller) prop or a `View` class's [controller](./view.md#controller) prop to specify that viewport interaction should be enabled.

`FirstPersonController` is the default controller for [FirstPersonView](./first-person-view.md).
`FirstPersonController` is the default controller for [FirstPersonView](./first-person-view.md). It simulates the movement of a human being, with the scroll motion moving forward/backwards and dragging rotating the head.

## Usage

Expand Down Expand Up @@ -37,9 +37,10 @@ new Deck({

Supports all [Controller options](./controller.md#options) with the following default behavior:

- `dragMode`: default `'rotate'` (drag to rotate)
- `dragPan`: not effective, this view does not support panning
- `keyboard`: arrow keys to move camera, arrow keys with shift/ctrl down to rotate, +/- to zoom
- `dragMode`: default `'rotate'` (drag to rotate, shift-drag to pan)
- `dragPan`: default `true` (supported only from v9.0)
- `keyboard`: arrow keys to move camera, arrow keys with shift/ctrl down to rotate, +/- to move vertically
- `scrollZoom`: scroll to move in direction of mouse pointer, in horizontal 2D plane


## Custom FirstPersonController
Expand Down
92 changes: 72 additions & 20 deletions modules/core/src/controllers/first-person-controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Controller from './controller';
import ViewState from './view-state';
import {mod} from '../utils/math-utils';
import type Viewport from '../viewports/viewport';
import LinearInterpolator from '../transitions/linear-interpolator';

import {Vector3, _SphericalCoordinates as SphericalCoordinates, clamp} from '@math.gl/core';

const MOVEMENT_SPEED = 20;
const PAN_SPEED = 500;

type FirstPersonStateProps = {
width: number;
Expand All @@ -28,14 +30,23 @@ type FirstPersonStateInternal = {
startBearing?: number;
startPitch?: number;
startZoomPosition?: number[];
startPanPos?: [number, number];
startPanPosition?: number[];
};

class FirstPersonState extends ViewState<
FirstPersonState,
FirstPersonStateProps,
FirstPersonStateInternal
> {
constructor(options: FirstPersonStateProps & FirstPersonStateInternal) {
makeViewport: (props: Record<string, any>) => Viewport;

constructor(
options: FirstPersonStateProps &
FirstPersonStateInternal & {
makeViewport: (props: Record<string, any>) => Viewport;
}
) {
const {
/* Viewport arguments */
width, // Width of viewport
Expand All @@ -58,7 +69,9 @@ class FirstPersonState extends ViewState<
startRotatePos,
startBearing,
startPitch,
startZoomPosition
startZoomPosition,
startPanPos,
startPanPosition
} = options;

super(
Expand All @@ -77,9 +90,13 @@ class FirstPersonState extends ViewState<
startRotatePos,
startBearing,
startPitch,
startZoomPosition
startZoomPosition,
startPanPos,
startPanPosition
}
);

this.makeViewport = options.makeViewport;
}

/* Public API */
Expand All @@ -88,24 +105,48 @@ class FirstPersonState extends ViewState<
* Start panning
* @param {[Number, Number]} pos - position on screen where the pointer grabs
*/
panStart(): FirstPersonState {
return this;
panStart({pos}): FirstPersonState {
const {position} = this.getViewportProps();
return this._getUpdatedState({
startPanPos: pos,
startPanPosition: position
});
}

/**
* Pan
* @param {[Number, Number]} pos - position on screen where the pointer is
*/
pan(): FirstPersonState {
return this;
pan({pos}): FirstPersonState {
if (!pos) {
return this;
}
const {startPanPos = [0, 0], startPanPosition = [0, 0]} = this.getState();
const {width, height, bearing, pitch} = this.getViewportProps();
const deltaScaleX = (PAN_SPEED * (pos[0] - startPanPos[0])) / width;
const deltaScaleY = (PAN_SPEED * (pos[1] - startPanPos[1])) / height;

const up = new SphericalCoordinates({bearing, pitch});
const forward = new SphericalCoordinates({bearing, pitch: -90});
const yDirection = up.toVector3().normalize();
const xDirection = forward.toVector3().cross(yDirection).normalize();

return this._getUpdatedState({
position: new Vector3(startPanPosition)
.add(xDirection.scale(deltaScaleX))
.add(yDirection.scale(deltaScaleY))
});
}

/**
* End panning
* Must call if `panStart()` was called
*/
panEnd(): FirstPersonState {
return this;
return this._getUpdatedState({
startPanPos: null,
startPanPosition: null
});
}

/**
Expand Down Expand Up @@ -188,14 +229,20 @@ class FirstPersonState extends ViewState<
* @param {Number} scale - a number between [0, 1] specifying the accumulated
* relative scale.
*/
zoom({scale}: {scale: number}): FirstPersonState {
let {startZoomPosition} = this.getState();
if (!startZoomPosition) {
startZoomPosition = this.getViewportProps().position;
}
zoom({pos, scale}: {pos: [number, number]; scale: number}): FirstPersonState {
const viewportProps = this.getViewportProps();
const startZoomPosition = this.getState().startZoomPosition || viewportProps.position;
const viewport = this.makeViewport(viewportProps);
const {projectionMatrix, width} = viewport;
const fovxRadians = 2.0 * Math.atan(1.0 / projectionMatrix[0]);
const angle = fovxRadians * (pos[0] / width - 0.5);

const direction = this.getDirection();
return this._move(direction, Math.log2(scale) * MOVEMENT_SPEED, startZoomPosition);
const direction = this.getDirection(true);
return this._move(
direction.rotateZ({radians: -angle}),
Math.log2(scale) * MOVEMENT_SPEED,
startZoomPosition
);
}

/**
Expand Down Expand Up @@ -254,12 +301,12 @@ class FirstPersonState extends ViewState<
});
}

zoomIn(speed: number = 2): FirstPersonState {
return this.zoom({scale: speed});
zoomIn(speed: number = MOVEMENT_SPEED): FirstPersonState {
return this._move(new Vector3(0, 0, 1), speed);
}

zoomOut(speed: number = 2): FirstPersonState {
return this.zoom({scale: 1 / speed});
zoomOut(speed: number = MOVEMENT_SPEED): FirstPersonState {
return this._move(new Vector3(0, 0, -1), speed);
}

// shortest path between two view states
Expand Down Expand Up @@ -304,7 +351,12 @@ class FirstPersonState extends ViewState<

_getUpdatedState(newProps: Record<string, any>): FirstPersonState {
// Update _viewportProps
return new FirstPersonState({...this.getViewportProps(), ...this.getState(), ...newProps});
return new FirstPersonState({
makeViewport: this.makeViewport,
...this.getViewportProps(),
...this.getState(),
...newProps
});
}

// Apply any constraints (mathematical or defined by _viewportProps) to map state
Expand Down
Loading