(filed w/ GPT 5.6 Sol, edited by me)
Describe the bug
Axonometric projection does not move the projected camera target to the logical viewport center defined by CameraOptions::padding.
Transform::moveBy() still calculates movement from the padded viewport center. The projection and gesture calculations therefore disagree when the padding is asymmetric. Panning can move in an unexpected direction, and bearing or pitch changes orbit around the physical viewport center instead of the padded center.
To Reproduce
- Create a
Transform with a 512 × 512 viewport.
- Configure a camera with a 160 px left inset.
- Enable axonometric projection.
- Project the configured camera center to the screen.
- Call
moveBy(), or change the bearing and pitch while preserving the padded-center landmark.
For example:
transform.resize({512, 512});
transform.jumpTo(
CameraOptions()
.withCenter(LatLng{40.7128, -74.0060})
.withZoom(15.0)
.withBearing(25.0)
.withPitch(45.0)
.withPadding(EdgeInsets{0.0, 160.0, 0.0, 0.0}));
transform.setProjectionMode(
ProjectionMode()
.withAxonometric(true)
.withXSkew(0.0)
.withYSkew(1.0));
The logical viewport center is (336, 256), but axonometric projection does not apply the inset offset.
A downstream reproduction and regression test are available in MapLibre Compose PR #1227:
maplibre/maplibre-compose#1227
Expected behavior
Axonometric projection honors EdgeInsets in the same way as perspective projection. The configured camera center projects to the padded viewport center. moveBy() follows the requested screen-space delta, and bearing or pitch changes preserve the landmark under that center.
Screenshots
Not applicable.
Platform information (please complete the following information):
- Operating System: macOS
- Platform: Core C++, observed through
maplibre-native-ffi and MapLibre Compose
maplibre-native-ffi: 0.202608.3
- MapLibre Native commit in that release:
550f64be2232e09934ffc660a9acdbacff8b164a
- Also present on MapLibre Native
main: e5d0fce32a98ec94303ab18ae3da46581b0edd48
Additional context
TransformState::getProjMatrix() applies the edge-inset offset only when axonometric is false:
|
// Move the center of perspective to center of specified edgeInsets. |
|
// Values are in range [-1, 1] where the upper and lower range values |
|
// position viewport center to the screen edges. This is overridden |
|
// if using axonometric perspective (not in public API yet, Issue #11882). |
|
// TODO(astojilj): Issue #11882 should take edge insets into account, too. |
|
if (!axonometric) { |
|
cameraToClip[8] = -offset.x * 2.0 / size.width; |
|
cameraToClip[9] = offset.y * 2.0 / size.height; |
|
} |
|
|
|
// Apply north orientation angle |
|
if (getNorthOrientation() != NorthOrientation::Upwards) { |
|
matrix::rotate_z(cameraToClip, cameraToClip, -getNorthOrientationAngle()); |
|
} |
|
|
|
matrix::multiply(projMatrix, cameraToClip, worldToCamera); |
|
|
|
if (axonometric) { |
|
// mat[11] controls perspective |
|
projMatrix[11] = 0.0; |
|
|
|
// mat[8], mat[9] control x-skew, y-skew |
|
double pixelsPerMeter = 1.0 / Projection::getMetersPerPixelAtLatitude(getLatLng().latitude(), getZoom()); |
|
projMatrix[8] = xSkew * pixelsPerMeter; |
|
projMatrix[9] = ySkew * pixelsPerMeter; |
|
} |
Transform::moveBy() nevertheless starts from edgeInsets.getCenter():
|
void Transform::moveBy(const ScreenCoordinate& offset, const AnimationOptions& animation) { |
|
ScreenCoordinate centerOffset = {offset.x, offset.y}; |
|
|
|
// Reduce the offset so that it never goes past the horizon. If it goes past |
|
// the horizon, the pan direction is opposite of the intended direction. |
|
const double pitch = state.getPitch(); |
|
const double offsetLength = std::hypot(offset.x, offset.y); |
|
if (pitch > 0.0 && offsetLength > 0.0) { |
|
const double cameraToCenter = 0.5 * static_cast<double>(state.getSize().height) / |
|
std::tan(state.getFieldOfView() / 2.0); |
|
const double pixelsToHorizon = std::abs(cameraToCenter / std::tan(pitch)); |
|
constexpr double horizonFactor = 0.75; // must be < 1 to keep the offset short of the horizon |
|
const double scale = pixelsToHorizon * horizonFactor / offsetLength; |
|
if (scale < 1.0) { |
|
centerOffset = {offset.x * scale, offset.y * scale}; |
|
} |
|
} |
|
|
|
ScreenCoordinate pointOnScreen = state.getEdgeInsets().getCenter(state.getSize().width, state.getSize().height) - |
|
centerOffset; |
|
// Use unwrapped LatLng to carry information about moveBy direction. |
|
easeTo(CameraOptions().withCenter(screenCoordinateToLatLng(pointOnScreen, LatLng::Unwrapped)), animation); |
|
} |
An archived Mapbox GL Native discussion predicted this conflict (@1ec5):
mapbox/mapbox-gl-native#11882 (comment)
This issue is latent because only the Node bindings expose an axonometric perspective option, while only the mobile SDKs expose content inset options. But as soon as we implement the feature requested here, we’ll need to address the overridden projection matrix.
(filed w/ GPT 5.6 Sol, edited by me)
Describe the bug
Axonometric projection does not move the projected camera target to the logical viewport center defined by
CameraOptions::padding.Transform::moveBy()still calculates movement from the padded viewport center. The projection and gesture calculations therefore disagree when the padding is asymmetric. Panning can move in an unexpected direction, and bearing or pitch changes orbit around the physical viewport center instead of the padded center.To Reproduce
Transformwith a 512 × 512 viewport.moveBy(), or change the bearing and pitch while preserving the padded-center landmark.For example:
transform.resize({512, 512}); transform.jumpTo( CameraOptions() .withCenter(LatLng{40.7128, -74.0060}) .withZoom(15.0) .withBearing(25.0) .withPitch(45.0) .withPadding(EdgeInsets{0.0, 160.0, 0.0, 0.0})); transform.setProjectionMode( ProjectionMode() .withAxonometric(true) .withXSkew(0.0) .withYSkew(1.0));The logical viewport center is
(336, 256), but axonometric projection does not apply the inset offset.A downstream reproduction and regression test are available in MapLibre Compose PR #1227:
maplibre/maplibre-compose#1227
Expected behavior
Axonometric projection honors
EdgeInsetsin the same way as perspective projection. The configured camera center projects to the padded viewport center.moveBy()follows the requested screen-space delta, and bearing or pitch changes preserve the landmark under that center.Screenshots
Not applicable.
Platform information (please complete the following information):
maplibre-native-ffiand MapLibre Composemaplibre-native-ffi: 0.202608.3550f64be2232e09934ffc660a9acdbacff8b164amain:e5d0fce32a98ec94303ab18ae3da46581b0edd48Additional context
TransformState::getProjMatrix()applies the edge-inset offset only whenaxonometricis false:maplibre-native/src/mln/map/transform_state.cpp
Lines 163 to 188 in e5d0fce
Transform::moveBy()nevertheless starts fromedgeInsets.getCenter():maplibre-native/src/mln/map/transform.cpp
Lines 406 to 428 in e5d0fce
An archived Mapbox GL Native discussion predicted this conflict (@1ec5):
mapbox/mapbox-gl-native#11882 (comment)