Skip to content

Conversation

@larsmaxfield
Copy link
Contributor

@larsmaxfield larsmaxfield commented Nov 1, 2025

Bug

In v5.10.0 transformConstrain is ignored when terrain is enabled. See CodePen example.

Terrain relies on cloning the transform, but cloning does not pass the custom constrain.

Problem

The apply() call in clone() does not set constrain, meaning cloned transforms don't have the (potentially) custom constrain.

If we set the old constrain to the new one, we might actually set an unchanged default constrain. This is a problem when we switch projections because we don't want to set the old default to an unrelated transform. (Mercator's default is different than Globe's, for example).

Proposal

Redesign constrain and transformConstrain:

  • Add transformConstrain to ITransformGetters
  • Change TransformHelper.constrain() to return transformConstrain if defined, otherwise return defaultConstrain. We do not overwrite constrain() and it cannot be set.
  • Apply transformConstrain in TransformHelper.apply(), ensuring all transform clones retain the custom constrain.

Launch Checklist

  • Confirm your changes do not include backports from Mapbox projects (unless with compliant license) - if you are not sure about this, please ask!
  • Briefly describe the changes in this PR.
  • Write tests for all new functionality.
  • Add an entry to CHANGELOG.md under the ## main section.

@larsmaxfield larsmaxfield marked this pull request as draft November 1, 2025 11:09
@larsmaxfield
Copy link
Contributor Author

Switched to draft. Need to check the failing tests before review.

@larsmaxfield larsmaxfield changed the title Add missing constrain setter in TransformHelper.apply Fix custom constrain not being passed in clone() Nov 1, 2025
@codecov
Copy link

codecov bot commented Nov 1, 2025

Codecov Report

❌ Patch coverage is 89.58333% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.52%. Comparing base (25d189b) to head (6f2a73d).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...c/geo/projection/vertical_perspective_transform.ts 57.14% 3 Missing ⚠️
src/geo/projection/globe_transform.ts 71.42% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6642   +/-   ##
=======================================
  Coverage   92.51%   92.52%           
=======================================
  Files         283      283           
  Lines       23389    23401   +12     
  Branches     5068     5071    +3     
=======================================
+ Hits        21639    21652   +13     
+ Misses       1750     1749    -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@larsmaxfield
Copy link
Contributor Author

I originally targeted TransformHelper.apply() to copy the transform constrain:

    public apply(thatI: ITransformGetters, constrain?: boolean, forceOverrideZ?: boolean): void {
        this._constrain = thatI.constrain;

However, this copies thatI.constrain regardless of it being custom or default: if you switch projections from a default Mercator to Globe, the resultant apply() will set the default Mercator constrain to the Globe. That's why those globe tests failed.

We need to set the constrain only if it differs from the default because that indicates it is custom. But we can't get the default from thatI, so we would need to somehow pass it to apply(). I propose instead doing this in each transform's clone() before it calls apply().

@larsmaxfield larsmaxfield marked this pull request as ready for review November 1, 2025 15:07
@HarelM
Copy link
Collaborator

HarelM commented Nov 1, 2025

Unfortunately, this apply/clone is too complicated. But it might be that the way we decided to store the constrain is also complicated... IDK...
I would recommend trying to fully understand what's going on there before trying to hack a solution...

@larsmaxfield
Copy link
Contributor Author

Fair. And if we define "apply" as "apply everything from this transform", then the current implementation isn't doing that because it skips the constrain.

@larsmaxfield larsmaxfield marked this pull request as draft November 1, 2025 16:03
@larsmaxfield larsmaxfield changed the title Fix custom constrain not being passed in clone() Redesign constrain(), transformConstrain; fix missing transformConstrain in apply() Nov 1, 2025
@larsmaxfield larsmaxfield marked this pull request as ready for review November 1, 2025 17:41
@larsmaxfield
Copy link
Contributor Author

larsmaxfield commented Nov 1, 2025

The design now sets transformConstrain separate from constrain.

constrain is no longer set, but rather returns transformConstrain if defined, otherwise returns defaultTransform.

*This means transformConstrain is a property like renderWorldCopies, rather than something that overwrites constrain.

Use defaultConstrain, transformConstrain appropriately
@larsmaxfield
Copy link
Contributor Author

larsmaxfield commented Nov 1, 2025

Renamed the constrains. Perhaps a bit verbose, but I want to make it clear what is happening with this approach:

  • getConstrain: Always call this to get the applicable constrain
  • defaultConstrain: The default constrain function defined by each transform (MercatorTransform, etc.)
  • transformConstrain: The optional user-specified function that getConstrain calls instead of defaultConstrain if it is defined

*I think I was using "override" incorrectly in my previous comments to describe what transformConstrain does. It doesn't change defaultConstrain, rather it is what getConstrain calls instead of defaultConstrain

@HarelM
Copy link
Collaborator

HarelM commented Nov 2, 2025

While there might be a different way to solve this, this is probably the most straight forward one.
But there's a need to make the names a lot more clearer in what they do.

transformConstrain -> constrainOverride
@larsmaxfield
Copy link
Contributor Author

larsmaxfield commented Nov 3, 2025

Names updated:

  • applyConstrain() to apply the applicable constrain to the center and zoom
  • constrainOverride as the optional callback that is used instead of defaultConstrain, if defined

I previously named the override "transformConstrain" because I read it as "the constrain that transforms the default constrain". But because it belongs to the transform, calling it transformConstrain is indeed not clear. I think "constrainOverride" is clearest now.

Let me know if I interpreted your comments correctly.

@larsmaxfield larsmaxfield changed the title Redesign constrain(), transformConstrain; fix missing transformConstrain in apply() Fix missing constrainOverride setter in TransformHelper.apply Nov 3, 2025
*/
defaultConstrain: TransformConstrainFunction;

/**
Copy link
Collaborator

@HarelM HarelM Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the above defaultConstrain be part of the interface? I would imagine that this is something more internal...?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, GlobeTransform relies on it being available when it calls it from this.currentTransform (ITransform):

    public constructor(options?: TransformOptions) {
        this._helper = new TransformHelper({
            calcMatrices: () => { this._calcMatrices(); },
            defaultConstrain: (center, zoom) => { return this.defaultConstrain(center, zoom); }

    ...

    defaultConstrain: TransformConstrainFunction = (lngLat, zoom) => {
        return this.currentTransform.defaultConstrain(lngLat, zoom);
    };

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the best design, but I guess it would do.

@HarelM
Copy link
Collaborator

HarelM commented Nov 4, 2025

Thanks! This looks good now.
I've added one last question before this can be merged.

@HarelM HarelM merged commit 50cc7ef into maplibre:main Nov 4, 2025
26 checks passed
@larsmaxfield
Copy link
Contributor Author

Thanks again for your advice and review!

@larsmaxfield larsmaxfield deleted the fix/transform-constrain-terrain branch November 5, 2025 10:30
melitele pushed a commit to melitele/maplibre-gl-js that referenced this pull request Jan 8, 2026
…plibre#6642)

* Add missing constrain set in TransformHelper.apply

* Add CHANGELOG entry for apply fix

* Fix lint

* Set constrain in clone (not apply)

Only set if the transform's constrain differs from its defaultConstrain

* Add transform clone tests for custom constrain

* Update CHANGELOG

* Revert "Update CHANGELOG"

This reverts commit cd4673b.

* Revert "Add transform clone tests for custom constrain"

This reverts commit cc96d8a.

* Revert "Set constrain in clone (not apply)"

This reverts commit 980e6ec.

* Revert "Fix lint"

This reverts commit 9974452.

* Revert "Add CHANGELOG entry for apply fix"

This reverts commit aee339c.

* Revert "Add missing constrain set in TransformHelper.apply"

This reverts commit e1d8ed1.

* Redesign constrain, transformConstrain in Helper

Return transformConstrain if defined, otherwise defaultConstrain

* Use nullish coalescing operator, not OR

* Clearer constrain names; constrain -> getConstrain

Use defaultConstrain, transformConstrain appropriately

* Clearer internal constrain names -> applyConstrain

transformConstrain -> constrainOverride

* Add CHANELOG entry

---------

Co-authored-by: Harel M <harel.mazor@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants