[core] Offset viewport center when edge insets are specified
The change is implemented in TransformState::getProjMatrix, the rest of the code is making sure that existing API contracts stay and there are tests verifyingrendering and render query processing only items within screen and given tolerance around screen edges. MapView: don't bake edge insets into relalculated camera center. Keep edge insets as property of camera in TransformState (similar to pitch, zoom, bearing) independent from specified camera center. Interpolate edge insets in animation. iOS Demo app: "Turn On/Off Content Insets" pitch the camera and navigate to convenient location in Denver, where streets are parallel to cardinal directions, to illustrate viewport center offset when edge insets are set. Tests: ViewFrustumCulling: although Annotations are deprecated, queryRenderedFeatures related tests in Annotations would need to get ported and decided to add the edge insets related query tests next to them. Verify frustum culling (render+queryRenderedFeatures) With different camera and edge insets setups. TODO: port Annotations tests. Transform.Padding: Verify that coordinates take proper place on screen after applying edge insets. LocalGlyphRasterizer: verify text rendering when applying padding. Related to #11882: both use projection matrix elements [8] and [9]. Alternative approach to this was to increase and offset map origin so that the screen would be a sub-rectangle in larger map viewport. This approach has a drawback of unecessary processing the items that are outside screen area. Fixes #12107, #12728, navigation-sdks/issues/120
This commit is contained in:
@@ -16,7 +16,7 @@ namespace mbgl {
|
||||
*/
|
||||
struct CameraOptions {
|
||||
CameraOptions& withCenter(const optional<LatLng>& o) { center = o; return *this; }
|
||||
CameraOptions& withPadding(const EdgeInsets& p) { padding = p; return *this; }
|
||||
CameraOptions& withPadding(const optional<EdgeInsets>& p) { padding = p; return *this; }
|
||||
CameraOptions& withAnchor(const optional<ScreenCoordinate>& o) { anchor = o; return *this; }
|
||||
CameraOptions& withZoom(const optional<double>& o) { zoom = o; return *this; }
|
||||
CameraOptions& withBearing(const optional<double>& o) { bearing = o; return *this; }
|
||||
@@ -27,7 +27,7 @@ struct CameraOptions {
|
||||
|
||||
/** Padding around the interior of the view that affects the frame of
|
||||
reference for `center`. */
|
||||
EdgeInsets padding;
|
||||
optional<EdgeInsets> padding;
|
||||
|
||||
/** Point of reference for `zoom` and `angle`, assuming an origin at the
|
||||
top-left corner of the view. */
|
||||
|
||||
@@ -105,6 +105,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
|
||||
MBXSettingsMiscellaneousShowSnapshots,
|
||||
MBXSettingsMiscellaneousMissingIcon,
|
||||
MBXSettingsMiscellaneousShouldLimitCameraChanges,
|
||||
MBXSettingsMiscellaneousSetContentInsets,
|
||||
MBXSettingsMiscellaneousShowCustomLocationManager,
|
||||
MBXSettingsMiscellaneousOrnamentsPlacement,
|
||||
MBXSettingsMiscellaneousPrintLogFile,
|
||||
@@ -209,6 +210,7 @@ CLLocationCoordinate2D randomWorldCoordinate() {
|
||||
@property (nonatomic) BOOL shouldLimitCameraChanges;
|
||||
@property (nonatomic) BOOL randomWalk;
|
||||
@property (nonatomic) NSMutableArray<UIWindow *> *helperWindows;
|
||||
@property (nonatomic) NSMutableArray<UIView *> *contentInsetsOverlays;
|
||||
|
||||
@end
|
||||
|
||||
@@ -219,6 +221,8 @@ CLLocationCoordinate2D randomWorldCoordinate() {
|
||||
@implementation MBXViewController
|
||||
{
|
||||
BOOL _isTouringWorld;
|
||||
BOOL _contentInsetsEnabled;
|
||||
UIEdgeInsets _originalContentInsets;
|
||||
}
|
||||
|
||||
#pragma mark - Setup & Teardown
|
||||
@@ -475,6 +479,7 @@ CLLocationCoordinate2D randomWorldCoordinate() {
|
||||
@"Show Snapshots",
|
||||
@"Missing Icon",
|
||||
[NSString stringWithFormat:@"%@ Camera Changes", (_shouldLimitCameraChanges ? @"Unlimit" : @"Limit")],
|
||||
[NSString stringWithFormat:@"Turn %@ Content Insets", (_contentInsetsEnabled ? @"Off" : @"On")],
|
||||
@"View Route Simulation",
|
||||
@"Ornaments Placement",
|
||||
]];
|
||||
@@ -739,6 +744,61 @@ CLLocationCoordinate2D randomWorldCoordinate() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MBXSettingsMiscellaneousSetContentInsets:
|
||||
{
|
||||
if (!_contentInsetsEnabled) {
|
||||
_originalContentInsets = [self.mapView contentInset];
|
||||
}
|
||||
_contentInsetsEnabled = !_contentInsetsEnabled;
|
||||
self.automaticallyAdjustsScrollViewInsets = !_contentInsetsEnabled;
|
||||
UIEdgeInsets contentInsets = self.mapView.bounds.size.width > self.mapView.bounds.size.height
|
||||
? UIEdgeInsetsMake(_originalContentInsets.top, 0.5 * self.mapView.bounds.size.width, _originalContentInsets.bottom, 0.0)
|
||||
: UIEdgeInsetsMake(0.25 * self.mapView.bounds.size.height, 0.0, _originalContentInsets.bottom, 0.25 * self.mapView.bounds.size.width);
|
||||
if (_contentInsetsEnabled) {
|
||||
if (!self.contentInsetsOverlays)
|
||||
self.contentInsetsOverlays = [NSMutableArray array];
|
||||
if (![self.contentInsetsOverlays count]) {
|
||||
UIView *view = [[UIView alloc]initWithFrame:CGRectMake(0, 0, self.mapView.bounds.size.width, contentInsets.top)];
|
||||
view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5];
|
||||
[self.contentInsetsOverlays addObject:view];
|
||||
[self.view addSubview:view];
|
||||
view = [[UIView alloc]initWithFrame:CGRectMake(0, 0, contentInsets.left, self.mapView.bounds.size.height)];
|
||||
view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5];
|
||||
[self.contentInsetsOverlays addObject:view];
|
||||
[self.view addSubview:view];
|
||||
view = [[UIView alloc]initWithFrame:CGRectMake(self.mapView.bounds.size.width - contentInsets.right, 0, contentInsets.right, self.mapView.bounds.size.height)];
|
||||
view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5];
|
||||
[self.contentInsetsOverlays addObject:view];
|
||||
[self.view addSubview:view];
|
||||
view = [[UIView alloc]initWithFrame:CGRectMake(0, self.mapView.bounds.size.height - contentInsets.bottom, self.mapView.bounds.size.width, self.mapView.bounds.size.height)];
|
||||
view.backgroundColor = [UIColor colorWithRed:0.0 green:0.3 blue:0.3 alpha:0.5];
|
||||
[self.contentInsetsOverlays addObject:view];
|
||||
[self.view addSubview:view];
|
||||
}
|
||||
[self.view bringSubviewToFront:self.contentInsetsOverlays[0]];
|
||||
[self.view bringSubviewToFront:self.contentInsetsOverlays[1]];
|
||||
[self.view bringSubviewToFront:self.contentInsetsOverlays[2]];
|
||||
[self.view bringSubviewToFront:self.contentInsetsOverlays[3]];
|
||||
|
||||
// Denver streets parallel to cardinal directions help illustrate
|
||||
// viewport center offset when edge insets are set.
|
||||
MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:CLLocationCoordinate2DMake(39.72707, -104.9986)
|
||||
acrossDistance:100
|
||||
pitch:60
|
||||
heading:0];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[self.mapView setCamera:camera withDuration:0.3 animationTimingFunction:nil completionHandler:^{
|
||||
[weakSelf.mapView setContentInset:contentInsets animated:TRUE];
|
||||
}];
|
||||
} else {
|
||||
[self.view sendSubviewToBack:self.contentInsetsOverlays[0]];
|
||||
[self.view sendSubviewToBack:self.contentInsetsOverlays[1]];
|
||||
[self.view sendSubviewToBack:self.contentInsetsOverlays[2]];
|
||||
[self.view sendSubviewToBack:self.contentInsetsOverlays[3]];
|
||||
[self.mapView setContentInset:_originalContentInsets animated:TRUE];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MBXSettingsMiscellaneousOrnamentsPlacement:
|
||||
{
|
||||
MBXOrnamentsViewController *ornamentsViewController = [[MBXOrnamentsViewController alloc] init];
|
||||
@@ -2005,6 +2065,23 @@ CLLocationCoordinate2D randomWorldCoordinate() {
|
||||
[sender setAccessibilityValue:nextAccessibilityValue];
|
||||
}
|
||||
|
||||
#pragma mark - UIViewDelegate
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
|
||||
{
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||
if (_contentInsetsEnabled)
|
||||
{
|
||||
_contentInsetsEnabled = NO;
|
||||
self.automaticallyAdjustsScrollViewInsets = YES;
|
||||
[self.mapView setContentInset:UIEdgeInsetsZero];
|
||||
}
|
||||
while (self.contentInsetsOverlays && [self.contentInsetsOverlays count]) {
|
||||
[[self.contentInsetsOverlays lastObject] removeFromSuperview];
|
||||
[self.contentInsetsOverlays removeLastObject];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MGLMapViewDelegate
|
||||
|
||||
- (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id<MGLAnnotation>)annotation
|
||||
|
||||
@@ -1076,20 +1076,15 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
// After adjusting the content inset, move the center coordinate from the
|
||||
// old frame of reference to the new one represented by the newly set
|
||||
// content inset.
|
||||
CLLocationCoordinate2D oldCenter = self.centerCoordinate;
|
||||
|
||||
_contentInset = contentInset;
|
||||
|
||||
if (self.userTrackingMode == MGLUserTrackingModeNone)
|
||||
{
|
||||
// Don’t call -setCenterCoordinate:, which resets the user tracking mode.
|
||||
[self _setCenterCoordinate:oldCenter animated:animated];
|
||||
[self _setCenterCoordinate:self.centerCoordinate edgePadding:contentInset zoomLevel:self.zoomLevel direction:self.direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:NULL];
|
||||
_contentInset = contentInset;
|
||||
}
|
||||
else
|
||||
{
|
||||
_contentInset = contentInset;
|
||||
[self didUpdateLocationWithUserTrackingAnimated:animated];
|
||||
}
|
||||
|
||||
@@ -3313,10 +3308,6 @@ public:
|
||||
[self _setCenterCoordinate:centerCoordinate edgePadding:self.contentInset zoomLevel:zoomLevel direction:direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:completion];
|
||||
}
|
||||
|
||||
- (void)_setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate animated:(BOOL)animated {
|
||||
[self _setCenterCoordinate:centerCoordinate edgePadding:self.contentInset zoomLevel:self.zoomLevel direction:self.direction duration:animated ? MGLAnimationDuration : 0 animationTimingFunction:nil completionHandler:NULL];
|
||||
}
|
||||
|
||||
- (void)_setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate edgePadding:(UIEdgeInsets)insets zoomLevel:(double)zoomLevel direction:(CLLocationDirection)direction duration:(NSTimeInterval)duration animationTimingFunction:(nullable CAMediaTimingFunction *)function completionHandler:(nullable void (^)(void))completion
|
||||
{
|
||||
if (!_mbglMap)
|
||||
@@ -3356,7 +3347,7 @@ public:
|
||||
}
|
||||
|
||||
MGLMapCamera *camera = [self cameraForCameraOptions:cameraOptions];
|
||||
if ([self.camera isEqualToMapCamera:camera])
|
||||
if ([self.camera isEqualToMapCamera:camera] && UIEdgeInsetsEqualToEdgeInsets(_contentInset, insets))
|
||||
{
|
||||
if (completion)
|
||||
{
|
||||
@@ -3893,7 +3884,7 @@ public:
|
||||
return self.residualCamera;
|
||||
}
|
||||
|
||||
mbgl::CameraOptions mapCamera = self.mbglMap.getCameraOptions();
|
||||
mbgl::CameraOptions mapCamera = self.mbglMap.getCameraOptions(cameraOptions.padding.value_or(mbgl::EdgeInsets()));
|
||||
CLLocationCoordinate2D centerCoordinate = MGLLocationCoordinate2DFromLatLng(cameraOptions.center ? *cameraOptions.center : *mapCamera.center);
|
||||
double zoomLevel = cameraOptions.zoom ? *cameraOptions.zoom : self.zoomLevel;
|
||||
CLLocationDirection direction = cameraOptions.bearing ? mbgl::util::wrap(*cameraOptions.bearing, 0., 360.) : self.direction;
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace mbgl {
|
||||
* For horizontal labels we want to do step 1 in the shader for performance reasons (no cpu work).
|
||||
* This is what `u_label_plane_matrix` is used for.
|
||||
* For labels aligned with lines we have to steps 1 and 2 on the cpu since we need access to the line geometry.
|
||||
* This is what `updateLineLabels(...)` does.
|
||||
* This is what `updateLineLabels(...)` in JS, `reprojectLineLabels()` in gl-native, does.
|
||||
* Since the conversion is handled on the cpu we just set `u_label_plane_matrix` to an identity matrix.
|
||||
*
|
||||
* Steps 3 and 4 are done in the shaders for all labels.
|
||||
|
||||
@@ -77,13 +77,13 @@ void Transform::jumpTo(const CameraOptions& camera) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Change any combination of center, zoom, bearing, and pitch, with a smooth animation
|
||||
* between old and new values. The map will retain the current values for any options
|
||||
* not included in `options`.
|
||||
* Change any combination of center, zoom, bearing, pitch and edgeInsets, with a
|
||||
* smooth animation between old and new values. The map will retain the current
|
||||
* values for any options not included in `options`.
|
||||
*/
|
||||
void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& animation) {
|
||||
const EdgeInsets& padding = camera.padding;
|
||||
LatLng startLatLng = getLatLng(padding, LatLng::Unwrapped);
|
||||
const EdgeInsets& padding = camera.padding.value_or(state.edgeInsets);
|
||||
LatLng startLatLng = getLatLng(LatLng::Unwrapped);
|
||||
const LatLng& unwrappedLatLng = camera.center.value_or(startLatLng);
|
||||
const LatLng& latLng = state.bounds != LatLngBounds::unbounded() ? unwrappedLatLng : unwrappedLatLng.wrapped();
|
||||
double zoom = camera.zoom.value_or(getZoom());
|
||||
@@ -110,9 +110,6 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim
|
||||
const Point<double> startPoint = Projection::project(startLatLng, state.scale);
|
||||
const Point<double> endPoint = Projection::project(latLng, state.scale);
|
||||
|
||||
ScreenCoordinate center = getScreenCoordinate(padding);
|
||||
center.y = state.size.height - center.y;
|
||||
|
||||
// Constrain camera options.
|
||||
zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
|
||||
const double scale = state.zoomScale(zoom);
|
||||
@@ -130,6 +127,7 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim
|
||||
state.panning = unwrappedLatLng != startLatLng;
|
||||
state.scaling = scale != startScale;
|
||||
state.rotating = bearing != startBearing;
|
||||
const EdgeInsets startEdgeInsets = state.edgeInsets;
|
||||
|
||||
startTransition(camera, animation, [=](double t) {
|
||||
Point<double> framePoint = util::interpolate(startPoint, endPoint, t);
|
||||
@@ -143,9 +141,14 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim
|
||||
if (pitch != startPitch) {
|
||||
state.pitch = util::interpolate(startPitch, pitch, t);
|
||||
}
|
||||
|
||||
if (!padding.isFlush()) {
|
||||
state.moveLatLng(frameLatLng, center);
|
||||
if (padding != startEdgeInsets) {
|
||||
// Interpolate edge insets
|
||||
state.edgeInsets = {
|
||||
util::interpolate(startEdgeInsets.top(), padding.top(), t),
|
||||
util::interpolate(startEdgeInsets.left(), padding.left(), t),
|
||||
util::interpolate(startEdgeInsets.bottom(), padding.bottom(), t),
|
||||
util::interpolate(startEdgeInsets.right(), padding.right(), t)
|
||||
};
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
@@ -159,8 +162,8 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim
|
||||
Where applicable, local variable documentation begins with the associated
|
||||
variable or function in van Wijk (2003). */
|
||||
void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &animation) {
|
||||
const EdgeInsets& padding = camera.padding;
|
||||
const LatLng& latLng = camera.center.value_or(getLatLng(padding, LatLng::Unwrapped)).wrapped();
|
||||
const EdgeInsets& padding = camera.padding.value_or(state.edgeInsets);
|
||||
const LatLng& latLng = camera.center.value_or(getLatLng(LatLng::Unwrapped)).wrapped();
|
||||
double zoom = camera.zoom.value_or(getZoom());
|
||||
double bearing = camera.bearing ? -*camera.bearing * util::DEG2RAD : getBearing();
|
||||
double pitch = camera.pitch ? *camera.pitch * util::DEG2RAD : getPitch();
|
||||
@@ -170,15 +173,12 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima
|
||||
}
|
||||
|
||||
// Determine endpoints.
|
||||
LatLng startLatLng = getLatLng(padding, LatLng::Unwrapped).wrapped();
|
||||
LatLng startLatLng = getLatLng(LatLng::Unwrapped).wrapped();
|
||||
startLatLng.unwrapForShortestPath(latLng);
|
||||
|
||||
const Point<double> startPoint = Projection::project(startLatLng, state.scale);
|
||||
const Point<double> endPoint = Projection::project(latLng, state.scale);
|
||||
|
||||
ScreenCoordinate center = getScreenCoordinate(padding);
|
||||
center.y = state.size.height - center.y;
|
||||
|
||||
// Constrain camera options.
|
||||
zoom = util::clamp(zoom, state.getMinZoom(), state.getMaxZoom());
|
||||
pitch = util::clamp(pitch, util::PITCH_MIN, util::PITCH_MAX);
|
||||
@@ -278,6 +278,7 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima
|
||||
state.panning = true;
|
||||
state.scaling = true;
|
||||
state.rotating = bearing != startBearing;
|
||||
const EdgeInsets startEdgeInsets = state.edgeInsets;
|
||||
|
||||
startTransition(camera, animation, [=](double k) {
|
||||
/// s: The distance traveled along the flight path, measured in
|
||||
@@ -304,9 +305,14 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima
|
||||
if (pitch != startPitch) {
|
||||
state.pitch = util::interpolate(startPitch, pitch, k);
|
||||
}
|
||||
|
||||
if (!padding.isFlush()) {
|
||||
state.moveLatLng(frameLatLng, center);
|
||||
if (padding != startEdgeInsets) {
|
||||
// Interpolate edge insets
|
||||
state.edgeInsets = {
|
||||
util::interpolate(startEdgeInsets.top(), padding.top(), us),
|
||||
util::interpolate(startEdgeInsets.left(), padding.left(), us),
|
||||
util::interpolate(startEdgeInsets.bottom(), padding.bottom(), us),
|
||||
util::interpolate(startEdgeInsets.right(), padding.right(), us)
|
||||
};
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
@@ -314,28 +320,16 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima
|
||||
#pragma mark - Position
|
||||
|
||||
void Transform::moveBy(const ScreenCoordinate& offset, const AnimationOptions& animation) {
|
||||
ScreenCoordinate centerOffset = { offset.x, -offset.y, };
|
||||
ScreenCoordinate centerPoint = getScreenCoordinate() - centerOffset;
|
||||
easeTo(CameraOptions().withCenter(state.screenCoordinateToLatLng(centerPoint)), animation);
|
||||
ScreenCoordinate centerOffset = { offset.x, offset.y };
|
||||
ScreenCoordinate pointOnScreen = state.edgeInsets.getCenter(state.size.width, state.size.height) - centerOffset;
|
||||
// Use unwrapped LatLng to carry information about moveBy direction.
|
||||
easeTo(CameraOptions().withCenter(screenCoordinateToLatLng(pointOnScreen, LatLng::Unwrapped)), animation);
|
||||
}
|
||||
|
||||
LatLng Transform::getLatLng(const EdgeInsets& padding, LatLng::WrapMode wrap) const {
|
||||
if (padding.isFlush()) {
|
||||
return state.getLatLng(wrap);
|
||||
} else {
|
||||
return screenCoordinateToLatLng(padding.getCenter(state.size.width, state.size.height));
|
||||
}
|
||||
LatLng Transform::getLatLng(LatLng::WrapMode wrap) const {
|
||||
return state.getLatLng(wrap);
|
||||
}
|
||||
|
||||
ScreenCoordinate Transform::getScreenCoordinate(const EdgeInsets& padding) const {
|
||||
if (padding.isFlush()) {
|
||||
return { state.size.width / 2., state.size.height / 2. };
|
||||
} else {
|
||||
return padding.getCenter(state.size.width, state.size.height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Zoom
|
||||
|
||||
double Transform::getZoom() const {
|
||||
@@ -364,7 +358,7 @@ void Transform::setMaxZoom(const double maxZoom) {
|
||||
#pragma mark - Bearing
|
||||
|
||||
void Transform::rotateBy(const ScreenCoordinate& first, const ScreenCoordinate& second, const AnimationOptions& animation) {
|
||||
ScreenCoordinate center = getScreenCoordinate();
|
||||
ScreenCoordinate center = state.edgeInsets.getCenter(state.size.width, state.size.height);
|
||||
const ScreenCoordinate offset = first - center;
|
||||
const double distance = std::sqrt(std::pow(2, offset.x) + std::pow(2, offset.y));
|
||||
|
||||
@@ -576,10 +570,10 @@ ScreenCoordinate Transform::latLngToScreenCoordinate(const LatLng& latLng) const
|
||||
return point;
|
||||
}
|
||||
|
||||
LatLng Transform::screenCoordinateToLatLng(const ScreenCoordinate& point) const {
|
||||
LatLng Transform::screenCoordinateToLatLng(const ScreenCoordinate& point, LatLng::WrapMode wrapMode) const {
|
||||
ScreenCoordinate flippedPoint = point;
|
||||
flippedPoint.y = state.size.height - flippedPoint.y;
|
||||
return state.screenCoordinateToLatLng(flippedPoint).wrapped();
|
||||
return state.screenCoordinateToLatLng(flippedPoint, wrapMode);
|
||||
}
|
||||
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -47,8 +47,7 @@ public:
|
||||
@param offset The distance to pan the map by, measured in pixels from
|
||||
top to bottom and from left to right. */
|
||||
void moveBy(const ScreenCoordinate& offset, const AnimationOptions& = {});
|
||||
LatLng getLatLng(const EdgeInsets& = {}, LatLng::WrapMode = LatLng::Wrapped) const;
|
||||
ScreenCoordinate getScreenCoordinate(const EdgeInsets& = {}) const;
|
||||
LatLng getLatLng(LatLng::WrapMode = LatLng::Wrapped) const;
|
||||
|
||||
// Bounds
|
||||
|
||||
@@ -105,7 +104,7 @@ public:
|
||||
|
||||
// Conversion and projection
|
||||
ScreenCoordinate latLngToScreenCoordinate(const LatLng&) const;
|
||||
LatLng screenCoordinateToLatLng(const ScreenCoordinate&) const;
|
||||
LatLng screenCoordinateToLatLng(const ScreenCoordinate&, LatLng::WrapMode = LatLng::Wrapped) const;
|
||||
|
||||
private:
|
||||
MapObserver& observer;
|
||||
|
||||
@@ -33,24 +33,36 @@ void TransformState::getProjMatrix(mat4& projMatrix, uint16_t nearZ, bool aligne
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the distance from the center point [width/2, height/2] to the
|
||||
// center top point [width/2, 0] in Z units, using the law of sines.
|
||||
const double cameraToCenterDistance = getCameraToCenterDistance();
|
||||
auto offset = getCenterOffset();
|
||||
|
||||
// Find the distance from the viewport center point
|
||||
// [width/2 + offset.x, height/2 + offset.y] to the top edge, to point
|
||||
// [width/2 + offset.x, 0] in Z units, using the law of sines.
|
||||
// 1 Z unit is equivalent to 1 horizontal px at the center of the map
|
||||
// (the distance between[width/2, height/2] and [width/2 + 1, height/2])
|
||||
const double halfFov = getFieldOfView() / 2.0;
|
||||
const double fovAboveCenter = getFieldOfView() * (0.5 + offset.y / size.height);
|
||||
const double groundAngle = M_PI / 2.0 + getPitch();
|
||||
const double topHalfSurfaceDistance = std::sin(halfFov) * getCameraToCenterDistance() / std::sin(M_PI - groundAngle - halfFov);
|
||||
const double aboveCenterSurfaceDistance = std::sin(fovAboveCenter) * cameraToCenterDistance / std::sin(M_PI - groundAngle - fovAboveCenter);
|
||||
|
||||
|
||||
// Calculate z distance of the farthest fragment that should be rendered.
|
||||
const double furthestDistance = std::cos(M_PI / 2 - getPitch()) * topHalfSurfaceDistance + getCameraToCenterDistance();
|
||||
const double furthestDistance = std::cos(M_PI / 2 - getPitch()) * aboveCenterSurfaceDistance + cameraToCenterDistance;
|
||||
// Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance`
|
||||
const double farZ = furthestDistance * 1.01;
|
||||
|
||||
matrix::perspective(projMatrix, getFieldOfView(), double(size.width) / size.height, nearZ, farZ);
|
||||
|
||||
// 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 overriden
|
||||
// if using axonometric perspective (not in public API yet, Issue #11882).
|
||||
// TODO(astojilj): Issue #11882 should take edge insets into account, too.
|
||||
projMatrix[8] = -offset.x * 2.0 / size.width;
|
||||
projMatrix[9] = offset.y * 2.0 / size.height;
|
||||
|
||||
const bool flippedY = viewportMode == ViewportMode::FlippedY;
|
||||
matrix::scale(projMatrix, projMatrix, 1, flippedY ? 1 : -1, 1);
|
||||
matrix::scale(projMatrix, projMatrix, 1.0, flippedY ? 1 : -1, 1);
|
||||
|
||||
matrix::translate(projMatrix, projMatrix, 0, 0, -getCameraToCenterDistance());
|
||||
|
||||
@@ -134,16 +146,8 @@ ViewportMode TransformState::getViewportMode() const {
|
||||
#pragma mark - Camera options
|
||||
|
||||
CameraOptions TransformState::getCameraOptions(const EdgeInsets& padding) const {
|
||||
LatLng center;
|
||||
if (padding.isFlush()) {
|
||||
center = getLatLng();
|
||||
} else {
|
||||
ScreenCoordinate point = padding.getCenter(size.width, size.height);
|
||||
point.y = size.height - point.y;
|
||||
center = screenCoordinateToLatLng(point).wrapped();
|
||||
}
|
||||
return CameraOptions()
|
||||
.withCenter(center)
|
||||
.withCenter(getLatLng())
|
||||
.withPadding(padding)
|
||||
.withZoom(getZoom())
|
||||
.withBearing(-bearing * util::RAD2DEG)
|
||||
@@ -222,6 +226,10 @@ double TransformState::getMaxZoom() const {
|
||||
return scaleZoom(max_scale);
|
||||
}
|
||||
|
||||
ScreenCoordinate TransformState::getCenterOffset() const {
|
||||
return { 0.5 * (edgeInsets.left() - edgeInsets.right()), 0.5 * (edgeInsets.top() - edgeInsets.bottom()) };
|
||||
}
|
||||
|
||||
#pragma mark - Rotation
|
||||
|
||||
float TransformState::getBearing() const {
|
||||
@@ -240,7 +248,6 @@ float TransformState::getPitch() const {
|
||||
return pitch;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - State
|
||||
|
||||
bool TransformState::isChanging() const {
|
||||
@@ -328,8 +335,7 @@ LatLng TransformState::screenCoordinateToLatLng(const ScreenCoordinate& point, L
|
||||
mat4 TransformState::coordinatePointMatrix() const {
|
||||
mat4 proj;
|
||||
getProjMatrix(proj);
|
||||
float s = util::tileSize;
|
||||
matrix::scale(proj, proj, s, s, 1);
|
||||
matrix::scale(proj, proj, util::tileSize, util::tileSize, 1);
|
||||
matrix::multiply(proj, getPixelMatrix(), proj);
|
||||
return proj;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ public:
|
||||
void setMaxZoom(double);
|
||||
double getMaxZoom() const;
|
||||
|
||||
// Viewport center offset, from [size.width / 2, size.height / 2], defined
|
||||
// by |edgeInsets| in screen coordinates, with top left origin.
|
||||
ScreenCoordinate getCenterOffset() const;
|
||||
|
||||
// Rotation
|
||||
float getBearing() const;
|
||||
float getFieldOfView() const;
|
||||
@@ -137,6 +141,8 @@ private:
|
||||
double ySkew = 1.0;
|
||||
bool axonometric = false;
|
||||
|
||||
EdgeInsets edgeInsets;
|
||||
|
||||
// cache values for spherical mercator math
|
||||
double Bc = Projection::worldSize(scale) / util::DEGREES_MAX;
|
||||
double Cc = Projection::worldSize(scale) / util::M2PI;
|
||||
|
||||
@@ -66,7 +66,7 @@ Values makeValues(const bool isText,
|
||||
const bool pitchWithMap = values.pitchAlignment == style::AlignmentType::Map;
|
||||
const bool rotateWithMap = values.rotationAlignment == style::AlignmentType::Map;
|
||||
|
||||
// Line label rotation happens in `updateLineLabels`
|
||||
// Line label rotation happens in `updateLineLabels`/`reprojectLineLabels``
|
||||
// Pitched point labels are automatically rotated by the labelPlaneMatrix projection
|
||||
// Unpitched point labels need to have their rotation applied after projection
|
||||
const bool rotateInShader = rotateWithMap && !pitchWithMap && !alongLine;
|
||||
|
||||
@@ -348,25 +348,28 @@ std::pair<float,float> CollisionIndex::projectAnchor(const mat4& posMatrix, cons
|
||||
std::pair<Point<float>,float> CollisionIndex::projectAndGetPerspectiveRatio(const mat4& posMatrix, const Point<float>& point) const {
|
||||
vec4 p = {{ point.x, point.y, 0, 1 }};
|
||||
matrix::transformMat4(p, p, posMatrix);
|
||||
auto offset = transformState.getCenterOffset();
|
||||
auto size = transformState.getSize();
|
||||
return std::make_pair(
|
||||
Point<float>(
|
||||
(((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding,
|
||||
(((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding
|
||||
(((p[0] / p[3] + 1) / 2) * size.width) + viewportPadding + offset.x,
|
||||
(((-p[1] / p[3] + 1) / 2) * size.height) + viewportPadding + offset.y
|
||||
),
|
||||
// See perspective ratio comment in symbol_sdf.vertex
|
||||
// We're doing collision detection in viewport space so we need
|
||||
// to scale down boxes in the distance
|
||||
0.5 + 0.5 * (transformState.getCameraToCenterDistance() / p[3])
|
||||
0.5 + 0.5 * transformState.getCameraToCenterDistance() / p[3]
|
||||
);
|
||||
}
|
||||
|
||||
Point<float> CollisionIndex::projectPoint(const mat4& posMatrix, const Point<float>& point) const {
|
||||
vec4 p = {{ point.x, point.y, 0, 1 }};
|
||||
matrix::transformMat4(p, p, posMatrix);
|
||||
return Point<float>(
|
||||
(((p[0] / p[3] + 1) / 2) * transformState.getSize().width) + viewportPadding,
|
||||
(((-p[1] / p[3] + 1) / 2) * transformState.getSize().height) + viewportPadding
|
||||
);
|
||||
auto offset = transformState.getCenterOffset();
|
||||
auto size = transformState.getSize();
|
||||
return Point<float> {
|
||||
static_cast<float>((((p[0] / p[3] + 1) / 2) * size.width) + viewportPadding + offset.x),
|
||||
static_cast<float>((((-p[1] / p[3] + 1) / 2) * size.height) + viewportPadding + offset.y) };
|
||||
}
|
||||
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -434,6 +434,73 @@ TEST(Annotations, VisibleFeatures) {
|
||||
EXPECT_EQ(features.size(), ids.size());
|
||||
}
|
||||
|
||||
TEST(Annotations, ViewFrustumCulling) {
|
||||
// The purpose of the test is to control that annotations outside screen
|
||||
// rectangle are not rendered for different camera setup, especially when
|
||||
// using edge insets - viewport center is then offsetted.
|
||||
|
||||
// Important premise of this test is "static const float viewportPadding = 100;"
|
||||
// as defined in collision_index.cpp: tests using edge insets are writen so that
|
||||
// padding is 128 (half of viewSize width). If increasing viewportPadding,
|
||||
// increase the padding in test cases below.
|
||||
AnnotationTest test;
|
||||
|
||||
auto viewSize = test.frontend.getSize();
|
||||
auto box = ScreenBox { {}, { double(viewSize.width), double(viewSize.height) } };
|
||||
|
||||
test.map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json"));
|
||||
test.map.addAnnotationImage(namedMarker("default_marker"));
|
||||
const LatLng center = { 5.0, 5.0 };
|
||||
test.map.jumpTo(CameraOptions().withCenter(center).withZoom(3.0));
|
||||
|
||||
LatLng tl = test.map.latLngForPixel(ScreenCoordinate(0, 0));
|
||||
LatLng br = test.map.latLngForPixel(ScreenCoordinate(viewSize.width, viewSize.height));
|
||||
|
||||
std::vector<LatLng> latLngs = {
|
||||
tl,
|
||||
test.map.latLngForPixel(ScreenCoordinate(viewSize.width, 0)),
|
||||
test.map.latLngForPixel(ScreenCoordinate(0, viewSize.height)),
|
||||
br,
|
||||
center};
|
||||
|
||||
std::vector<mbgl::AnnotationID> ids;
|
||||
for (auto latLng : latLngs) {
|
||||
ids.push_back(test.map.addAnnotation(SymbolAnnotation { { latLng.longitude(), latLng.latitude() }, "default_marker" }));
|
||||
}
|
||||
|
||||
std::vector<std::pair<CameraOptions, std::vector<uint64_t>>> expectedVisibleForCamera = {
|
||||
// Start with all markers visible.
|
||||
{ CameraOptions(), { 0, 1, 2, 3, 4 } },
|
||||
// Move center to topLeft: only former center and top left (now center) are visible.
|
||||
{ CameraOptions().withCenter(tl), { 0, 4 } },
|
||||
// Reset center. With pitch: only top row markers and center are visible.
|
||||
{ CameraOptions().withCenter(center).withPitch(45), { 0, 1, 4 } },
|
||||
// Reset pitch, and use padding to move viewport center: only topleft and center are visible.
|
||||
{ CameraOptions().withPitch(0).withPadding(EdgeInsets { viewSize.height * 0.5, viewSize.width * 0.5, 0, 0 }), { 0, 4 } },
|
||||
// Use opposite padding to move viewport center: only bottom right and center are visible.
|
||||
{ CameraOptions().withPitch(0).withPadding(EdgeInsets { 0, 0, viewSize.height * 0.5, viewSize.width * 0.5 }), { 3, 4 } },
|
||||
// Use top padding to move viewport center: top row and center are visible.
|
||||
{ CameraOptions().withPitch(0).withPadding(EdgeInsets { viewSize.height * 0.5, 0, 0, 0 }), { 0, 1, 4 } },
|
||||
// Use bottom padding: only bottom right and center are visible.
|
||||
{ CameraOptions().withPitch(0).withPadding(EdgeInsets { 0, 0, viewSize.height * 0.5, 0 }), { 2, 3, 4 } },
|
||||
// Left padding and pitch: top left, bottom left and center are visible.
|
||||
{ CameraOptions().withPitch(45).withPadding(EdgeInsets { 0, viewSize.width * 0.5, 0, 0 }), { 0, 2, 4 } }
|
||||
};
|
||||
|
||||
for (unsigned i = 0; i < expectedVisibleForCamera.size(); i++) {
|
||||
auto testCase = expectedVisibleForCamera[i];
|
||||
test.map.jumpTo(testCase.first);
|
||||
test.frontend.render(test.map);
|
||||
auto features = test.frontend.getRenderer()->queryRenderedFeatures(box, {});
|
||||
for (uint64_t id : testCase.second) { // testCase.second is vector of ids expected.
|
||||
EXPECT_NE(std::find_if(features.begin(), features.end(), [&id](auto feature) {
|
||||
return id == feature.id.template get<uint64_t>();
|
||||
}), features.end()) << "Point with id " << id << " is missing in test case " << i;
|
||||
EXPECT_EQ(features.size(), testCase.second.size()) << " in test case " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Annotations, TopOffsetPixels) {
|
||||
AnnotationTest test;
|
||||
|
||||
|
||||
BIN
test/fixtures/local_glyphs/no_local_with_content_insets/expected.png
vendored
Normal file
BIN
test/fixtures/local_glyphs/no_local_with_content_insets/expected.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
BIN
test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png
vendored
Normal file
BIN
test/fixtures/local_glyphs/no_local_with_content_insets_and_pitch/expected.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@@ -288,25 +288,48 @@ TEST(Transform, Padding) {
|
||||
|
||||
ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude());
|
||||
ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude());
|
||||
|
||||
transform.jumpTo(CameraOptions().withCenter(LatLng { 10, -100 }).withZoom(10.0));
|
||||
CameraOptions nonPaddedCameraOptions = CameraOptions().withCenter(LatLng { 10, -100 }).withZoom(10.0);
|
||||
transform.jumpTo(nonPaddedCameraOptions);
|
||||
|
||||
const LatLng trueCenter = transform.getLatLng();
|
||||
ASSERT_DOUBLE_EQ(10, trueCenter.latitude());
|
||||
ASSERT_DOUBLE_EQ(-100, trueCenter.longitude());
|
||||
ASSERT_DOUBLE_EQ(10, transform.getZoom());
|
||||
|
||||
const LatLng manualShiftedCenter = transform.getState().screenCoordinateToLatLng({
|
||||
const LatLng screenCenter = transform.screenCoordinateToLatLng({
|
||||
1000.0 / 2.0,
|
||||
1000.0 / 4.0,
|
||||
1000.0 / 2.0,
|
||||
});
|
||||
const LatLng upperHalfCenter = transform.screenCoordinateToLatLng({
|
||||
1000.0 / 2.0,
|
||||
1000.0 * 0.25,
|
||||
});
|
||||
|
||||
EdgeInsets padding(1000.0 / 2.0, 0, 0, 0);
|
||||
const LatLng shiftedCenter = transform.getLatLng(padding);
|
||||
ASSERT_NE(trueCenter.latitude(), shiftedCenter.latitude());
|
||||
ASSERT_NEAR(trueCenter.longitude(), shiftedCenter.longitude(), 1e-8);
|
||||
ASSERT_DOUBLE_EQ(manualShiftedCenter.latitude(), shiftedCenter.latitude());
|
||||
ASSERT_DOUBLE_EQ(manualShiftedCenter.longitude(), shiftedCenter.longitude());
|
||||
// CameraOption center and zoom don't change when padding changes: center of
|
||||
// viewport remains the same as padding defines viwport center offset in rendering.
|
||||
CameraOptions paddedOptions = CameraOptions().withPadding(padding);
|
||||
transform.jumpTo(paddedOptions);
|
||||
const LatLng theSameCenter = transform.getLatLng();
|
||||
ASSERT_DOUBLE_EQ(trueCenter.latitude(), theSameCenter.latitude());
|
||||
ASSERT_DOUBLE_EQ(trueCenter.longitude(), theSameCenter.longitude());
|
||||
|
||||
// However, LatLng is now at the center of lower half - verify conversion
|
||||
// from screen coordinate to LatLng.
|
||||
const LatLng paddedLowerHalfScreenCenter = transform.screenCoordinateToLatLng({
|
||||
1000.0 / 2.0,
|
||||
1000.0 * 0.75,
|
||||
});
|
||||
ASSERT_NEAR(screenCenter.latitude(), paddedLowerHalfScreenCenter.latitude(), 1e-10);
|
||||
ASSERT_NEAR(screenCenter.longitude(), paddedLowerHalfScreenCenter.longitude(), 1e-10);
|
||||
|
||||
// LatLng previously in upper half center, should now be under screen center.
|
||||
const LatLng paddedScreenCenter = transform.screenCoordinateToLatLng({
|
||||
1000.0 / 2.0,
|
||||
1000.0 / 2.0,
|
||||
});
|
||||
ASSERT_NEAR(upperHalfCenter.latitude(), paddedScreenCenter.latitude(), 1e-10);
|
||||
ASSERT_NEAR(upperHalfCenter.longitude(), paddedScreenCenter.longitude(), 1e-10);
|
||||
}
|
||||
|
||||
TEST(Transform, MoveBy) {
|
||||
@@ -440,7 +463,7 @@ TEST(Transform, Camera) {
|
||||
flyOptions.transitionFrameFn = [&](double t) {
|
||||
ASSERT_TRUE(t >= 0 && t <= 1);
|
||||
ASSERT_LE(latLng2.latitude(), transform.getLatLng().latitude());
|
||||
ASSERT_GE(latLng2.longitude(), transform.getLatLng({}, LatLng::Unwrapped).longitude());
|
||||
ASSERT_GE(latLng2.longitude(), transform.getLatLng(LatLng::Unwrapped).longitude());
|
||||
};
|
||||
flyOptions.transitionFinishFn = [&]() {
|
||||
// XXX Fix precision loss in flyTo:
|
||||
@@ -618,7 +641,7 @@ TEST(Transform, LatLngBounds) {
|
||||
|
||||
// Try crossing the antimeridian from the right.
|
||||
transform.jumpTo(CameraOptions().withCenter(LatLng { 0.0, 200.0 }));
|
||||
ASSERT_DOUBLE_EQ(transform.getLatLng({}, LatLng::Unwrapped).longitude(), 180.0);
|
||||
ASSERT_DOUBLE_EQ(transform.getLatLng(LatLng::Unwrapped).longitude(), 180.0);
|
||||
ASSERT_DOUBLE_EQ(transform.getLatLng().longitude(), -180.0);
|
||||
|
||||
// -1 | 0 | +1
|
||||
|
||||
@@ -43,9 +43,9 @@ public:
|
||||
MapAdapter map { frontend, MapObserver::nullObserver(), fileSource,
|
||||
MapOptions().withMapMode(MapMode::Static).withSize(frontend.getSize())};
|
||||
|
||||
void checkRendering(const char * name) {
|
||||
void checkRendering(const char * name, double imageMatchPixelsThreshold = 0.05, double pixelMatchThreshold = 0.1) {
|
||||
test::checkImage(std::string("test/fixtures/local_glyphs/") + name,
|
||||
frontend.render(map), 0.05, 0.1);
|
||||
frontend.render(map), imageMatchPixelsThreshold, pixelMatchThreshold);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,6 +85,50 @@ TEST(LocalGlyphRasterizer, NoLocal) {
|
||||
return response;
|
||||
};
|
||||
test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json"));
|
||||
test.checkRendering("no_local");
|
||||
test.checkRendering("no_local", 0.001, 0.1);
|
||||
}
|
||||
|
||||
TEST(LocalGlyphRasterizer, NoLocalWithContentInsets) {
|
||||
// Expectations: content insets imply rendering to offsetted viewport center.
|
||||
// Rendered text should be on the same offset and keep the same size as
|
||||
// with no offset.
|
||||
LocalGlyphRasterizerTest test({});
|
||||
|
||||
test.fileSource->glyphsResponse = [&] (const Resource& resource) {
|
||||
EXPECT_EQ(Resource::Kind::Glyphs, resource.kind);
|
||||
Response response;
|
||||
response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/glyphs.pbf"));
|
||||
return response;
|
||||
};
|
||||
auto viewSize = test.frontend.getSize();
|
||||
test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json"));
|
||||
|
||||
// Expected image was created using center offset as in the line below - content insets,
|
||||
// with no pitch defined, should produce the same output.
|
||||
// test.map.moveBy({ viewSize.width * 0.25, 0 });
|
||||
|
||||
test.map.jumpTo(CameraOptions().withPadding(EdgeInsets { 0, viewSize.width * 0.25 * 2, 0, 0 }));
|
||||
test.checkRendering("no_local_with_content_insets", 0.001, 0.1);
|
||||
}
|
||||
|
||||
TEST(LocalGlyphRasterizer, NoLocalWithContentInsetsAndPitch) {
|
||||
// Expectations: content insets imply rendering to offsetted viewport center.
|
||||
// Rendered text should be on the same offset and keep the same size as
|
||||
// with no offset.
|
||||
LocalGlyphRasterizerTest test({});
|
||||
|
||||
test.fileSource->glyphsResponse = [&] (const Resource& resource) {
|
||||
EXPECT_EQ(Resource::Kind::Glyphs, resource.kind);
|
||||
Response response;
|
||||
response.data = std::make_shared<std::string>(util::read_file("test/fixtures/resources/glyphs.pbf"));
|
||||
return response;
|
||||
};
|
||||
auto viewSize = test.frontend.getSize();
|
||||
test.map.getStyle().loadJSON(util::read_file("test/fixtures/local_glyphs/mixed.json"));
|
||||
|
||||
// Expected image was verified using no-padding render, offsetted to right
|
||||
// using bitmap editor.
|
||||
|
||||
test.map.jumpTo(CameraOptions().withPitch(45).withPadding(EdgeInsets { 0, viewSize.width * 0.4, 0, 0 }));
|
||||
test.checkRendering("no_local_with_content_insets_and_pitch", 0.001, 0.1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user