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

Fix unwanted roll #5083 #5092

Merged
merged 20 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- ⚠️ Change drag rotate behavior to be less abrupt around the center ([#5104](https://github.com/maplibre/maplibre-gl-js/pull/5104))
- Fix regression in render world copies ([#5101](https://github.com/maplibre/maplibre-gl-js/pull/5101))
- Fix unwanted roll when motion is interrupted ([#5083](https://github.com/maplibre/maplibre-gl-js/pull/5083))
- _...Add new stuff here..._

## 5.0.0-pre.8
Expand Down
71 changes: 51 additions & 20 deletions src/geo/projection/camera_helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {type LngLat, type LngLatLike} from '../lng_lat';
import {type CameraForBoundsOptions, type PointLike} from '../../ui/camera';
import {type PaddingOptions} from '../edge_insets';
import {type LngLatBounds} from '../lng_lat_bounds';
import {getRollPitchBearing, type RollPitchBearing, warnOnce} from '../../util/util';
import {getRollPitchBearing, type RollPitchBearing, rollPitchBearingToQuat, warnOnce} from '../../util/util';
import {quat} from 'gl-matrix';
import {interpolates} from '@maplibre/maplibre-gl-style-spec';

export type MapControlsDeltas = {
panDelta: Point;
Expand Down Expand Up @@ -61,6 +62,33 @@ export type FlyToHandlerResult = {
pixelPathLength: number;
}

export type UpdateRotationArgs = {
/**
* The starting Euler angles.
*/
startEulerAngles: RollPitchBearing;

/**
* The end Euler angles.
*/
endEulerAngles: RollPitchBearing;

/**
* The transform to be updated
*/
tr: ITransform;

/**
* The interpolation fraction, between 0 and 1.
*/
k: number;

/**
* If true, use spherical linear interpolation. If false, use linear interpolation of Euler angles.
*/
useSlerp: boolean;
}

/**
* @internal
*/
Expand Down Expand Up @@ -97,26 +125,29 @@ export interface ICameraHelper {

/**
* @internal
* Set a transform's rotation to a value interpolated between startRotation and endRotation
* @param startRotation - the starting rotation (rotation when k = 0)
* @param endRotation - the end rotation (rotation when k = 1)
* @param endEulerAngles - the end Euler angles. This is needed in case `endRotation` has an ambiguous Euler angle representation.
* @param tr - the transform to be updated
* @param k - the interpolation fraction, between 0 and 1.
* Set a transform's rotation to a value interpolated between startEulerAngles and endEulerAngles
*/
export function updateRotation(startRotation: quat, endRotation: quat, endEulerAngles: RollPitchBearing, tr: ITransform, k: number) {
// At pitch ==0, the Euler angle representation is ambiguous. In this case, set the Euler angles
// to the representation requested by the caller
if (k < 1) {
const rotation: quat = new Float64Array(4) as any;
quat.slerp(rotation, startRotation, endRotation, k);
const eulerAngles = getRollPitchBearing(rotation);
tr.setRoll(eulerAngles.roll);
tr.setPitch(eulerAngles.pitch);
tr.setBearing(eulerAngles.bearing);
export function updateRotation(args: UpdateRotationArgs) {
if (args.useSlerp) {
// At pitch ==0, the Euler angle representation is ambiguous. In this case, set the Euler angles
// to the representation requested by the caller
if (args.k < 1) {
const startRotation = rollPitchBearingToQuat(args.startEulerAngles.roll, args.startEulerAngles.pitch, args.startEulerAngles.bearing);
const endRotation = rollPitchBearingToQuat(args.endEulerAngles.roll, args.endEulerAngles.pitch, args.endEulerAngles.bearing);
const rotation: quat = new Float64Array(4) as any;
HarelM marked this conversation as resolved.
Show resolved Hide resolved
quat.slerp(rotation, startRotation, endRotation, args.k);
const eulerAngles = getRollPitchBearing(rotation);
args.tr.setRoll(eulerAngles.roll);
args.tr.setPitch(eulerAngles.pitch);
args.tr.setBearing(eulerAngles.bearing);
} else {
args.tr.setRoll(args.endEulerAngles.roll);
args.tr.setPitch(args.endEulerAngles.pitch);
args.tr.setBearing(args.endEulerAngles.bearing);
}
} else {
tr.setRoll(endEulerAngles.roll);
tr.setPitch(endEulerAngles.pitch);
tr.setBearing(endEulerAngles.bearing);
args.tr.setRoll(interpolates.number(args.startEulerAngles.roll, args.endEulerAngles.roll, args.k));
args.tr.setPitch(interpolates.number(args.startEulerAngles.pitch, args.endEulerAngles.pitch, args.k));
args.tr.setBearing(interpolates.number(args.startEulerAngles.bearing, args.endEulerAngles.bearing, args.k));
}
}
19 changes: 12 additions & 7 deletions src/geo/projection/globe_camera_helper.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import Point from '@mapbox/point-geometry';
import {type IReadonlyTransform, type ITransform} from '../transform_interface';
import {cameraBoundsWarning, type CameraForBoxAndBearingHandlerResult, type EaseToHandlerResult, type EaseToHandlerOptions, type FlyToHandlerResult, type FlyToHandlerOptions, type ICameraHelper, type MapControlsDeltas, updateRotation} from './camera_helper';
import {cameraBoundsWarning, type CameraForBoxAndBearingHandlerResult, type EaseToHandlerResult, type EaseToHandlerOptions, type FlyToHandlerResult, type FlyToHandlerOptions, type ICameraHelper, type MapControlsDeltas, updateRotation, type UpdateRotationArgs} from './camera_helper';
import {type GlobeProjection} from './globe';
import {LngLat, type LngLatLike} from '../lng_lat';
import {MercatorCameraHelper} from './mercator_camera_helper';
import {angularCoordinatesToSurfaceVector, computeGlobePanCenter, getGlobeRadiusPixels, getZoomAdjustment, globeDistanceOfLocationsPixels, interpolateLngLatForGlobe} from './globe_utils';
import {clamp, createVec3f64, differenceOfAnglesDegrees, remapSaturate, rollPitchBearingToQuat, warnOnce} from '../../util/util';
import {type mat4, quat, vec3} from 'gl-matrix';
import {clamp, createVec3f64, differenceOfAnglesDegrees, remapSaturate, rollPitchBearingEqual, warnOnce} from '../../util/util';
import {type mat4, vec3} from 'gl-matrix';
import {MAX_VALID_LATITUDE, normalizeCenter, scaleZoom, zoomScale} from '../transform_helper';
import {type CameraForBoundsOptions} from '../../ui/camera';
import {type LngLatBounds} from '../lng_lat_bounds';
Expand Down Expand Up @@ -262,11 +262,11 @@ export class GlobeCameraHelper implements ICameraHelper {

const startZoom = tr.zoom;
const startCenter = tr.center;
const startRotation = rollPitchBearingToQuat(tr.roll, tr.pitch, tr.bearing);
const startEulerAngles = {roll: tr.roll, pitch: tr.pitch, bearing: tr.bearing};
const endRoll = options.roll === undefined ? tr.roll : options.roll;
const endPitch = options.pitch === undefined ? tr.pitch : options.pitch;
const endBearing = options.bearing === undefined ? tr.bearing : options.bearing;
const endRotation = rollPitchBearingToQuat(endRoll, endPitch, endBearing);
const endEulerAngles = {roll: endRoll, pitch: endPitch, bearing: endBearing};

const optionsZoom = typeof options.zoom !== 'undefined';

Expand Down Expand Up @@ -317,8 +317,13 @@ export class GlobeCameraHelper implements ICameraHelper {
isZooming = (endZoomWithShift !== startZoom);

const easeFunc = (k: number) => {
if (!quat.equals(startRotation, endRotation)) {
updateRotation(startRotation, endRotation, {roll: endRoll, pitch: endPitch, bearing: endBearing}, tr, k);
if (!rollPitchBearingEqual(startEulerAngles, endEulerAngles)) {
updateRotation({
startEulerAngles,
endEulerAngles,
tr,
k,
useSlerp: startEulerAngles.roll != endEulerAngles.roll} as UpdateRotationArgs);
}

if (options.around) {
Expand Down
18 changes: 11 additions & 7 deletions src/geo/projection/mercator_camera_helper.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import Point from '@mapbox/point-geometry';
import {LngLat, type LngLatLike} from '../lng_lat';
import {type IReadonlyTransform, type ITransform} from '../transform_interface';
import {cameraBoundsWarning, type CameraForBoxAndBearingHandlerResult, type EaseToHandlerResult, type EaseToHandlerOptions, type FlyToHandlerResult, type FlyToHandlerOptions, type ICameraHelper, type MapControlsDeltas, updateRotation} from './camera_helper';
import {cameraBoundsWarning, type CameraForBoxAndBearingHandlerResult, type EaseToHandlerResult, type EaseToHandlerOptions, type FlyToHandlerResult, type FlyToHandlerOptions, type ICameraHelper, type MapControlsDeltas, updateRotation, type UpdateRotationArgs} from './camera_helper';
import {type CameraForBoundsOptions} from '../../ui/camera';
import {type PaddingOptions} from '../edge_insets';
import {type LngLatBounds} from '../lng_lat_bounds';
import {normalizeCenter, scaleZoom, zoomScale} from '../transform_helper';
import {degreesToRadians, rollPitchBearingToQuat} from '../../util/util';
import {degreesToRadians, rollPitchBearingEqual} from '../../util/util';
import {projectToWorldCoordinates, unprojectFromWorldCoordinates} from './mercator_utils';
import {interpolates} from '@maplibre/maplibre-gl-style-spec';
import {quat} from 'gl-matrix';

/**
* @internal
Expand Down Expand Up @@ -128,11 +127,11 @@ export class MercatorCameraHelper implements ICameraHelper {
handleEaseTo(tr: ITransform, options: EaseToHandlerOptions): EaseToHandlerResult {
const startZoom = tr.zoom;
const startPadding = tr.padding;
const startRotation = rollPitchBearingToQuat(tr.roll, tr.pitch, tr.bearing);
const startEulerAngles = {roll: tr.roll, pitch: tr.pitch, bearing: tr.bearing};
const endRoll = options.roll === undefined ? tr.roll : options.roll;
const endPitch = options.pitch === undefined ? tr.pitch : options.pitch;
const endBearing = options.bearing === undefined ? tr.bearing : options.bearing;
const endRotation = rollPitchBearingToQuat(endRoll, endPitch, endBearing);
const endEulerAngles = {roll: endRoll, pitch: endPitch, bearing: endBearing};

const optionsZoom = typeof options.zoom !== 'undefined';

Expand Down Expand Up @@ -160,8 +159,13 @@ export class MercatorCameraHelper implements ICameraHelper {
if (isZooming) {
tr.setZoom(interpolates.number(startZoom, endZoom, k));
}
if (!quat.equals(startRotation, endRotation)) {
updateRotation(startRotation, endRotation, {roll: endRoll, pitch: endPitch, bearing: endBearing}, tr, k);
if (!rollPitchBearingEqual(startEulerAngles, endEulerAngles)) {
updateRotation({
startEulerAngles,
endEulerAngles,
tr,
k,
useSlerp: startEulerAngles.roll != endEulerAngles.roll} as UpdateRotationArgs);
}
if (doPadding) {
tr.interpolatePadding(startPadding, options.padding, k);
Expand Down
52 changes: 52 additions & 0 deletions src/ui/camera.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,58 @@ describe('#flyTo', () => {
expect(fixedLngLat(camera.getCenter())).toEqual({lng: 100, lat: 0});
});

test('no roll when motion is interrupted', () => {
const stub = vi.spyOn(browser, 'now');

const camera = createCamera();
stub.mockImplementation(() => 0);
camera.easeTo({pitch: 10, bearing: 100, duration: 1000});
stub.mockImplementation(() => 100);
camera.simulateFrame();
camera.easeTo({elevation: 1, duration: 0});
expect(camera.getRoll()).toBe(0);
});

test('no roll when motion is interrupted: globe', () => {
const stub = vi.spyOn(browser, 'now');

const camera = createCameraGlobe();
stub.mockImplementation(() => 0);
camera.easeTo({pitch: 10, bearing: 100, duration: 1000});
stub.mockImplementation(() => 100);
camera.simulateFrame();
camera.easeTo({elevation: 1, duration: 0});
expect(camera.getRoll()).toBe(0);
});

test('angles when motion is interrupted', () => {
const stub = vi.spyOn(browser, 'now');

const camera = createCamera();
stub.mockImplementation(() => 0);
camera.easeTo({pitch: 10, bearing: 20, roll: 30, duration: 1000});
stub.mockImplementation(() => 500);
camera.simulateFrame();
camera.easeTo({elevation: 1, duration: 0});
expect(camera.getRoll()).toBeCloseTo(25.041890412598942);
expect(camera.getPitch()).toBeCloseTo(8.116189398053095);
expect(camera.getBearing()).toBeCloseTo(15.041890412599061);
});

test('angles when motion is interrupted: globe', () => {
const stub = vi.spyOn(browser, 'now');

const camera = createCameraGlobe();
stub.mockImplementation(() => 0);
camera.easeTo({pitch: 10, bearing: 20, roll: 30, duration: 1000});
stub.mockImplementation(() => 500);
camera.simulateFrame();
camera.easeTo({elevation: 1, duration: 0});
expect(camera.getRoll()).toBeCloseTo(25.041890412598942);
expect(camera.getPitch()).toBeCloseTo(8.116189398053095);
expect(camera.getBearing()).toBeCloseTo(15.041890412599061);
});

test('can be called from within a moveend event handler', async () => {
const camera = createCamera();
const stub = vi.spyOn(browser, 'now');
Expand Down
4 changes: 4 additions & 0 deletions src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,10 @@ export type RollPitchBearing = {
bearing: number;
};

export function rollPitchBearingEqual(a: RollPitchBearing, b: RollPitchBearing): boolean {
HarelM marked this conversation as resolved.
Show resolved Hide resolved
return a.roll == b.roll && a.pitch == b.pitch && a.bearing == b.bearing;
}

/**
* This method converts a rotation quaternion to roll, pitch, and bearing angles in degrees.
* @param rotation - The rotation quaternion
Expand Down
Loading