Skip to content

Commit 4d0ff4e

Browse files
georginahalpernGeorgina Halpern
andauthored
Geospatial Camera Keyboard Inputs and PinchToZoom (#17466)
- Add keyboard inputs for geocam (pan/rotate/zoom) - Add pinchToZoom touch functionality #17451 Co-authored-by: Georgina Halpern <gehalper@microsoft.com>
1 parent 56fc471 commit 4d0ff4e

File tree

4 files changed

+260
-8
lines changed

4 files changed

+260
-8
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import type { Nullable } from "../../types";
2+
import { serialize } from "../../Misc/decorators";
3+
import type { Observer } from "../../Misc/observable";
4+
import type { Scene } from "../../scene";
5+
import type { GeospatialCamera } from "../geospatialCamera";
6+
import type { ICameraInput } from "../cameraInputsManager";
7+
import { CameraInputTypes } from "../cameraInputsManager";
8+
import type { KeyboardInfo } from "../../Events/keyboardEvents";
9+
import { KeyboardEventTypes } from "../../Events/keyboardEvents";
10+
import { Tools } from "../../Misc/tools";
11+
import type { AbstractEngine } from "../../Engines/abstractEngine";
12+
13+
/**
14+
* Manage the keyboard inputs to control the movement of a geospatial camera.
15+
* @see https://doc.babylonjs.com/features/featuresDeepDive/cameras/customizingCameraInputs
16+
*/
17+
export class GeospatialCameraKeyboardInput implements ICameraInput<GeospatialCamera> {
18+
/**
19+
* Defines the camera the input is attached to.
20+
*/
21+
public camera: GeospatialCamera;
22+
23+
/**
24+
* Defines the list of key codes associated with the up action (pan up)
25+
*/
26+
@serialize()
27+
public keysUp = [38];
28+
29+
/**
30+
* Defines the list of key codes associated with the down action (pan down)
31+
*/
32+
@serialize()
33+
public keysDown = [40];
34+
35+
/**
36+
* Defines the list of key codes associated with the left action (pan left)
37+
*/
38+
@serialize()
39+
public keysLeft = [37];
40+
41+
/**
42+
* Defines the list of key codes associated with the right action (pan right)
43+
*/
44+
@serialize()
45+
public keysRight = [39];
46+
47+
/**
48+
* Defines the list of key codes associated with zoom in (+ or =)
49+
*/
50+
@serialize()
51+
public keysZoomIn = [187, 107]; // 187 = + key, 107 = numpad +
52+
53+
/**
54+
* Defines the list of key codes associated with zoom out (-)
55+
*/
56+
@serialize()
57+
public keysZoomOut = [189, 109]; // 189 = - key, 109 = numpad -
58+
59+
/**
60+
* Defines the rotation sensitivity of the inputs.
61+
* (How many pixels of pointer input to apply per keypress, before rotation speed factor is applied by movement class)
62+
*/
63+
@serialize()
64+
public rotationSensitivity = 1.0;
65+
66+
/**
67+
* Defines the panning sensitivity of the inputs.
68+
* (How many pixels of pointer input to apply per keypress, before pan speed factor is applied by movement class)
69+
*/
70+
@serialize()
71+
public panSensitivity: number = 1.0;
72+
73+
/**
74+
* Defines the zooming sensitivity of the inputs.
75+
* (How many pixels of pointer input to apply per keypress, before zoom speed factor is applied by movement class)
76+
*/
77+
@serialize()
78+
public zoomSensitivity: number = 1.0;
79+
80+
private _keys = new Array<number>();
81+
private _ctrlPressed: boolean;
82+
private _onCanvasBlurObserver: Nullable<Observer<AbstractEngine>>;
83+
private _onKeyboardObserver: Nullable<Observer<KeyboardInfo>>;
84+
private _engine: AbstractEngine;
85+
private _scene: Scene;
86+
87+
/**
88+
* Attach the input controls to a specific dom element to get the input from.
89+
* @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
90+
*/
91+
public attachControl(noPreventDefault?: boolean): void {
92+
// was there a second variable defined?
93+
noPreventDefault = Tools.BackCompatCameraNoPreventDefault(arguments);
94+
95+
if (this._onCanvasBlurObserver) {
96+
return;
97+
}
98+
99+
this._onCanvasBlurObserver = this._engine.onCanvasBlurObservable.add(() => {
100+
this._keys.length = 0;
101+
});
102+
103+
this._onKeyboardObserver = this._scene.onKeyboardObservable.add((info) => {
104+
const evt = info.event;
105+
if (!evt.metaKey) {
106+
if (info.type === KeyboardEventTypes.KEYDOWN) {
107+
this._ctrlPressed = evt.ctrlKey;
108+
109+
if (
110+
this.keysUp.indexOf(evt.keyCode) !== -1 ||
111+
this.keysDown.indexOf(evt.keyCode) !== -1 ||
112+
this.keysLeft.indexOf(evt.keyCode) !== -1 ||
113+
this.keysRight.indexOf(evt.keyCode) !== -1 ||
114+
this.keysZoomIn.indexOf(evt.keyCode) !== -1 ||
115+
this.keysZoomOut.indexOf(evt.keyCode) !== -1
116+
) {
117+
const index = this._keys.indexOf(evt.keyCode);
118+
119+
if (index === -1) {
120+
this._keys.push(evt.keyCode);
121+
}
122+
123+
if (evt.preventDefault) {
124+
if (!noPreventDefault) {
125+
evt.preventDefault();
126+
}
127+
}
128+
}
129+
} else {
130+
if (
131+
this.keysUp.indexOf(evt.keyCode) !== -1 ||
132+
this.keysDown.indexOf(evt.keyCode) !== -1 ||
133+
this.keysLeft.indexOf(evt.keyCode) !== -1 ||
134+
this.keysRight.indexOf(evt.keyCode) !== -1 ||
135+
this.keysZoomIn.indexOf(evt.keyCode) !== -1 ||
136+
this.keysZoomOut.indexOf(evt.keyCode) !== -1
137+
) {
138+
const index = this._keys.indexOf(evt.keyCode);
139+
140+
if (index >= 0) {
141+
this._keys.splice(index, 1);
142+
}
143+
144+
if (evt.preventDefault) {
145+
if (!noPreventDefault) {
146+
evt.preventDefault();
147+
}
148+
}
149+
}
150+
}
151+
}
152+
});
153+
}
154+
155+
/**
156+
* Detach the current controls from the specified dom element.
157+
*/
158+
public detachControl(): void {
159+
if (this._scene) {
160+
if (this._onKeyboardObserver) {
161+
this._scene.onKeyboardObservable.remove(this._onKeyboardObserver);
162+
}
163+
if (this._onCanvasBlurObserver) {
164+
this._engine.onCanvasBlurObservable.remove(this._onCanvasBlurObserver);
165+
}
166+
this._onKeyboardObserver = null;
167+
this._onCanvasBlurObserver = null;
168+
}
169+
170+
this._keys.length = 0;
171+
}
172+
173+
/**
174+
* Update the current camera state depending on the inputs that have been used this frame.
175+
* This is a dynamically created lambda to avoid the performance penalty of looping for inputs in the render loop.
176+
*/
177+
public checkInputs(): void {
178+
if (this._onKeyboardObserver) {
179+
const camera = this.camera;
180+
181+
for (let index = 0; index < this._keys.length; index++) {
182+
const keyCode = this._keys[index];
183+
if (this._ctrlPressed) {
184+
// Rotation
185+
if (this.keysLeft.indexOf(keyCode) !== -1) {
186+
camera.movement.rotationAccumulatedPixels.y -= this.rotationSensitivity;
187+
} else if (this.keysRight.indexOf(keyCode) !== -1) {
188+
camera.movement.rotationAccumulatedPixels.y += this.rotationSensitivity;
189+
} else if (this.keysUp.indexOf(keyCode) !== -1) {
190+
camera.movement.rotationAccumulatedPixels.x -= this.rotationSensitivity;
191+
} else if (this.keysDown.indexOf(keyCode) !== -1) {
192+
camera.movement.rotationAccumulatedPixels.x += this.rotationSensitivity;
193+
}
194+
} else {
195+
// Zoom
196+
if (this.keysZoomIn.indexOf(keyCode) !== -1) {
197+
camera.movement.zoomAccumulatedPixels += this.zoomSensitivity;
198+
} else if (this.keysZoomOut.indexOf(keyCode) !== -1) {
199+
camera.movement.zoomAccumulatedPixels -= this.zoomSensitivity;
200+
} else {
201+
// Call into movement class handleDrag so that behavior matches that of pointer input, simulating drag from center of screen
202+
const centerX = this._engine.getRenderWidth() / 2;
203+
const centerY = this._engine.getRenderHeight() / 2;
204+
camera.movement.startDrag(centerX, centerY);
205+
if (this.keysLeft.indexOf(keyCode) !== -1) {
206+
camera.movement.handleDrag(centerX - this.panSensitivity, centerY);
207+
} else if (this.keysRight.indexOf(keyCode) !== -1) {
208+
camera.movement.handleDrag(centerX + this.panSensitivity, centerY);
209+
} else if (this.keysUp.indexOf(keyCode) !== -1) {
210+
camera.movement.handleDrag(centerX, centerY + this.panSensitivity);
211+
} else if (this.keysDown.indexOf(keyCode) !== -1) {
212+
camera.movement.handleDrag(centerX, centerY - this.panSensitivity);
213+
}
214+
camera.movement.stopDrag();
215+
}
216+
}
217+
}
218+
}
219+
}
220+
221+
/**
222+
* Gets the class name of the current input.
223+
* @returns the class name
224+
*/
225+
public getClassName(): string {
226+
return "GeospatialCameraKeyboardInput";
227+
}
228+
229+
/**
230+
* Get the friendly name associated with the input class.
231+
* @returns the input friendly name
232+
*/
233+
public getSimpleName(): string {
234+
return "keyboard";
235+
}
236+
}
237+
238+
(<any>CameraInputTypes)["GeospatialCameraKeyboardInput"] = GeospatialCameraKeyboardInput;

packages/dev/core/src/Cameras/Inputs/geospatialCameraPointersInput.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ export class GeospatialCameraPointersInput extends OrbitCameraPointersInput {
4848
public override onTouch(point: Nullable<PointerTouch>, offsetX: number, offsetY: number): void {
4949
// Single finger touch (no button property) or left button (button 0) = drag
5050
const button = point?.button ?? 0; // Default to button 0 (drag) if undefined
51-
51+
const scene = this.camera.getScene();
5252
switch (button) {
5353
case 0: // Left button / single touch - drag/pan globe under cursor
54-
this._handleDrag();
54+
this.camera.movement.handleDrag(scene.pointerX, scene.pointerY);
5555
break;
5656
case 1: // Middle button - tilt camera
5757
case 2: // Right button - tilt camera
@@ -60,6 +60,15 @@ export class GeospatialCameraPointersInput extends OrbitCameraPointersInput {
6060
}
6161
}
6262

63+
/**
64+
* Move camera from multitouch (pinch) zoom distances.
65+
* @param previousPinchSquaredDistance
66+
* @param pinchSquaredDistance
67+
*/
68+
protected override _computePinchZoom(previousPinchSquaredDistance: number, pinchSquaredDistance: number): void {
69+
this.camera.radius = (this.camera.radius * Math.sqrt(previousPinchSquaredDistance)) / Math.sqrt(pinchSquaredDistance);
70+
}
71+
6372
/**
6473
* Move camera from multi touch panning positions.
6574
* In geospatialcamera, multi touch panning tilts the globe (whereas single touch will pan/drag it)
@@ -94,11 +103,6 @@ export class GeospatialCameraPointersInput extends OrbitCameraPointersInput {
94103
super.onButtonUp(_evt);
95104
}
96105

97-
private _handleDrag(): void {
98-
const scene = this.camera.getScene();
99-
this.camera.movement.handleDrag(scene.pointerX, scene.pointerY);
100-
}
101-
102106
private _handleTilt(deltaX: number, deltaY: number): void {
103107
this.camera.movement.rotationAccumulatedPixels.y -= deltaX; // yaw - looking side to side
104108
this.camera.movement.rotationAccumulatedPixels.x -= deltaY; // pitch - look up towards sky / down towards ground

packages/dev/core/src/Cameras/geospatialCamera.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class GeospatialCamera extends Camera {
4747

4848
this.pickPredicate = pickPredicate;
4949
this.inputs = new GeospatialCameraInputsManager(this);
50-
this.inputs.addMouse().addMouseWheel();
50+
this.inputs.addMouse().addMouseWheel().addKeyboard();
5151
}
5252

5353
private _center: Vector3 = new Vector3();

packages/dev/core/src/Cameras/geospatialCameraInputsManager.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CameraInputsManager } from "./cameraInputsManager";
22
import type { GeospatialCamera } from "./geospatialCamera";
33
import { GeospatialCameraPointersInput } from "./Inputs/geospatialCameraPointersInput";
44
import { GeospatialCameraMouseWheelInput } from "./Inputs/geospatialCameraMouseWheelInput";
5+
import { GeospatialCameraKeyboardInput } from "./Inputs/geospatialCameraKeyboardInput";
56

67
/**
78
* Default Inputs manager for the GeospatialCamera.
@@ -33,4 +34,13 @@ export class GeospatialCameraInputsManager extends CameraInputsManager<Geospatia
3334
this.add(new GeospatialCameraMouseWheelInput());
3435
return this;
3536
}
37+
38+
/**
39+
* Add mouse wheel input support to the input manager
40+
* @returns the current input manager
41+
*/
42+
public addKeyboard(): GeospatialCameraInputsManager {
43+
this.add(new GeospatialCameraKeyboardInput());
44+
return this;
45+
}
3646
}

0 commit comments

Comments
 (0)