From 21ea593dbb49625b5a20ca029c713f06aeb25424 Mon Sep 17 00:00:00 2001 From: Ragini Pandey Date: Thu, 26 Mar 2026 01:36:38 +0530 Subject: [PATCH] fix: cameraForBounds accounts for asymmetrical padding in center result cameraForBounds was not accounting for asymmetrical padding when computing the camera center. The _extendAABB method was averaging left/right and top/bottom padding values, making the AABB extension symmetric. This meant the AABB center never shifted to account for padding asymmetry, so the resulting camera center was the same regardless of padding distribution. The fix uses the actual padding values (left, right, top, bottom) directly instead of averaging them. The available viewport width/height calculation remains the same (left + right = padL + padR), so zoom is unaffected for equal total padding. The AABB center now shifts to account for the difference between left and right (or top and bottom) padding. Fixes #13645 --- src/ui/camera.ts | 11 ++++----- test/unit/ui/camera.test.ts | 46 +++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/ui/camera.ts b/src/ui/camera.ts index 819a2693d3a..ad606053602 100644 --- a/src/ui/camera.ts +++ b/src/ui/camera.ts @@ -782,13 +782,10 @@ class Camera extends Evented { const padB = options.padding.bottom || 0; const padT = options.padding.top || 0; - const halfScreenPadX = (padL + padR) * 0.5; - const halfScreenPadY = (padT + padB) * 0.5; - - const top = halfScreenPadY; - const left = halfScreenPadX; - const right = halfScreenPadX; - const bottom = halfScreenPadY; + const top = padT; + const left = padL; + const right = padR; + const bottom = padB; const width = tr.width - (left + right); const height = tr.height - (top + bottom); diff --git a/test/unit/ui/camera.test.ts b/test/unit/ui/camera.test.ts index 17b2f236dbb..a7f65917f65 100644 --- a/test/unit/ui/camera.test.ts +++ b/test/unit/ui/camera.test.ts @@ -1104,7 +1104,39 @@ describe('camera', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const transform = camera.cameraForBounds(bb, {padding: {top: 10, right: 75, bottom: 50, left: 25}, duration: 0}); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(fixedLngLat(transform.center, 4)).toEqual({lng: -100.5, lat: 34.7171}); + expect(fixedLngLat(transform.center, 4)).toEqual({lng: -96.5558, lat: 32.0833}); + }); + + test('asymmetrical left padding shifts center', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const camera = createCamera(); + const bb = [[-133, 16], [-68, 50]]; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + const noPadding = camera.cameraForBounds(bb, {padding: {top: 0, right: 0, bottom: 0, left: 0}}); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + const leftPadding = camera.cameraForBounds(bb, {padding: {top: 0, right: 0, bottom: 0, left: 100}}); + + // With left padding, the center shifts left (more negative lng) to offset the bounds + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument + expect(leftPadding.center.lng).toBeLessThan(noPadding.center.lng); + // Zoom should decrease to accommodate the padding + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument + expect(leftPadding.zoom).toBeLessThan(noPadding.zoom); + }); + + test('symmetric padding does not shift center', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const camera = createCamera(); + const bb = [[-133, 16], [-68, 50]]; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + const noPadding = camera.cameraForBounds(bb); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + const symPadding = camera.cameraForBounds(bb, {padding: {top: 50, right: 50, bottom: 50, left: 50}}); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(fixedLngLat(noPadding.center, 4)).toEqual(fixedLngLat(symPadding.center, 4)); }); test('bearing and asymmetrical padding', () => { @@ -1115,7 +1147,7 @@ describe('camera', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const transform = camera.cameraForBounds(bb, {bearing: 90, padding: {top: 10, right: 75, bottom: 50, left: 25}, duration: 0}); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(fixedLngLat(transform.center, 4)).toEqual({lng: -100.5, lat: 34.7171}); + expect(fixedLngLat(transform.center, 4)).toEqual({lng: -103.3761, lat: 31.7099}); }); test( @@ -1130,7 +1162,7 @@ describe('camera', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const transform = camera.cameraForBounds(bb, {bearing: 90, padding: {top: 10, right: 75, bottom: 50, left: 25}, duration: 0}); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(fixedLngLat(transform.center, 4)).toEqual({lng: -100.5, lat: 34.7171}); + expect(fixedLngLat(transform.center, 4)).toEqual({lng: -103.3761, lat: 31.7099}); } ); @@ -1164,7 +1196,7 @@ describe('camera', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const transform = camera.cameraForBounds(bb, {padding: {top: 10, right: 75, bottom: 50, left: 25}, offset: [0, 100]}); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(fixedLngLat(transform.center, 4)).toEqual({lng: -100.5, lat: 46.6292}); + expect(fixedLngLat(transform.center, 4)).toEqual({lng: -96.5558, lat: 44.4189}); }); test('bearing, asymmetrical padding, and offset', () => { @@ -1175,7 +1207,7 @@ describe('camera', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const transform = camera.cameraForBounds(bb, {bearing: 90, padding: {top: 10, right: 75, bottom: 50, left: 25}, offset: [0, 100], duration: 0}); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(fixedLngLat(transform.center, 4)).toEqual({lng: -100.5, lat: 45.6619}); + expect(fixedLngLat(transform.center, 4)).toEqual({lng: -103.3761, lat: 43.0929}); }); test('unable to fit', () => { @@ -1441,7 +1473,7 @@ describe('camera', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access camera.fitBounds(bb, {padding: {top: 10, right: 75, bottom: 50, left: 25}, duration: 0}); // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - expect(fixedLngLat(camera.getCenter(), 4)).toEqual({lng: -100.5, lat: 34.7171}); + expect(fixedLngLat(camera.getCenter(), 4)).toEqual({lng: -96.5558, lat: 32.0833}); }); test('padding object with pitch', () => { @@ -1452,7 +1484,7 @@ describe('camera', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access camera.fitBounds(bb, {padding: {top: 10, right: 75, bottom: 50, left: 25}, duration: 0, pitch: 30}); // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - expect(fixedLngLat(camera.getCenter(), 4)).toEqual({lng: -100.5, lat: 34.7171}); + expect(fixedLngLat(camera.getCenter(), 4)).toEqual({lng: -96.5558, lat: 32.4408}); // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access expect(camera.getPitch()).toEqual(30); });