Skip to content

fix: accept single-color expressions for colorArray properties#4297

Draft
dts wants to merge 2 commits into
maplibre:mainfrom
dts:fix/hillshade-color-array-interpolate
Draft

fix: accept single-color expressions for colorArray properties#4297
dts wants to merge 2 commits into
maplibre:mainfrom
dts:fix/hillshade-color-array-interpolate

Conversation

@dts

@dts dts commented May 15, 2026

Copy link
Copy Markdown
Contributor

This is a fix for issue #4296. There's an overly restrictive validation step that is preventing animated hillshade layers from rendering (e.g. animating opacity during a zoom between levels).

AI Disclosure: I used AI to create the minimal reproduction & the fix here, I am confident it fixes the underlying issue (I've tested it on my personal device).

Repro of bug: https://gitlab.com/dts1/maplibre-native-hillshade-repro

PR maplibre#3965 promoted hillshade-shadow-color and hillshade-highlight-color from
PaintProperty<Color> to PaintProperty<std::vector<Color>> to support
multidirectional hillshade. The style spec types these properties as
colorArray with single-color defaults (#000000 / #FFFFFF), and
maplibre-gl-js continues to accept both color and array<color> expression
outputs for them.

Before this change, parsing any interpolate expression that outputs a single
color against PropertyValue<std::vector<Color>> fails at style-parse time
with:

  Expected array<color> but found string instead.

This breaks virtually every published topographic basemap that animates
hillshade color over zoom (e.g. LINZ Basemaps topographic style), because
those styles use the canonical interpolate(zoom) -> color shape. The whole
style fails to parse, producing a blank/gray map on iOS, Android, and
maplibre-react-native >= 6.24.0.

The runtime ValueConverter<std::vector<Color>>::fromExpressionValue in
hillshade_conversions.cpp already wraps a single Color into a one-element
vector at evaluation time. The only barrier was the compile-time expected
expression output, which valueTypeToExpressionType<std::vector<Color>>()
unconditionally pins to array<color>.

This patch specializes the expression-parse branch for std::vector<Color>:
try parsing as color first (the spec-default single-color shape, which is
what gl-js itself accepts), and fall back to array<color> for explicit
multidirectional users.

Test plan:

  ./mbgl-test-runner --gtest_filter=StyleConversion.PropertyValue*

  - PropertyValueVectorColor_SingleColorInterpolate (the regression case)
  - PropertyValueVectorColor_MultidirectionalConstant
  - PropertyValueVectorColor_PlainColorConstant
  - PropertyValue (unchanged)

  All four pass. The broader 23 StyleConversion tests and 94 Expression
  and Hillshade tests also continue to pass.

Repro: https://gitlab.com/dts1/maplibre-native-hillshade-repro
@louwers

louwers commented May 15, 2026

Copy link
Copy Markdown
Member

Please make sure to follow our AI Policy https://github.com/maplibre/maplibre/blob/main/AI_POLICY.md

@dts

dts commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

Shoot, I didn't look at that before, thanks for the heads-up - how would you like me to proceed? Rewrite the PR description and include an AI disclosure? I'm pretty confident in the solution, but it is AI-generated. I validated it as best I could (testing on physical devices, simulators, etc).

@louwers

louwers commented May 15, 2026

Copy link
Copy Markdown
Member

@dts No worries! It's pretty new and only mentioned in the contributor guidelines.

  1. If you can, just write the problem this solves in your own words.
  2. Briefly mention your process of fixing it and how you used AI.
  3. Take a critical look at the code and especially the comments. Is the solution appropriate? Does the test actually test the problem that was fixed? Is there any needless verbosity or is there a simpler fix?

Then, once you're confident. Mark the PR as ready for review. Then, a maintainer will have a look. And here be prepared to answer questions and/or follow up requests.

Does that sound reasonable?

@louwers louwers marked this pull request as draft May 15, 2026 16:19
@github-actions

Copy link
Copy Markdown
Contributor

Bloaty Results 🐋

Compared to main

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +58.9Ki  +0.0% +10.7Ki    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results/pr-4297-compared-to-main.txt

Compared to d387090 (legacy)

    FILE SIZE        VM SIZE    
 --------------  -------------- 
   +51% +59.2Mi  +467% +27.9Mi    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results/pr-4297-compared-to-legacy.txt

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results ⚡

Benchmark                                                     Time             CPU      Time Old      Time New       CPU Old       CPU New
------------------------------------------------------------------------------------------------------------------------------------------
OVERALL_GEOMEAN                                            -0.0055         -0.0056             0             0             0             0

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/benchmark-results/pr-4297-compared-to-main.txt

@github-actions

Copy link
Copy Markdown
Contributor

Bloaty Results (iOS) 🐋

Compared to main

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +1.52Ki  [ = ]       0    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results-ios/pr-4297-compared-to-main.txt

PR maplibre#3965 also promoted hillshade-illumination-direction and
hillshade-illumination-altitude from PaintProperty<float> to
PaintProperty<std::vector<float>>. The style spec types these as
numberArray with single-number defaults (335 and 45). The same parse-time
mismatch that broke colorArray properties also rejects spec-valid
single-number interpolate expressions for these:

    Expected array<number> but found number instead.

Unlike the colorArray case, maplibre#3965 did not add the runtime coercion either.
The primary template ValueConverter<std::vector<T>>::fromExpressionValue
only accepts array-shaped Values; a scalar Value falls through and returns
nullopt. So the property holder silently substitutes the spec default at
every frame -- strictly worse than the parse-time error.

This commit closes both halves of the gap for std::vector<float>, mirroring
the colorArray fix from the previous commit:

1. src/mbgl/style/conversion/property_value.cpp
   Generalizes the if constexpr branch to also match std::vector<float>.
   Element type derived via T::value_type, scalar/array context types via
   valueTypeToExpressionType<Element>().

2. src/mbgl/style/expression/value.cpp
   Adds the missing ValueConverter<std::vector<float>>::fromExpressionValue
   explicit specialization. Mirrors the existing std::vector<Color> one in
   hillshade_conversions.cpp: array branch identical to the primary template,
   plus a single-number branch that wraps into a one-element vector.

Test plan:

  ./mbgl-test-runner --gtest_filter=StyleConversion.PropertyValue*

  - PropertyValueVectorFloat_SingleNumberInterpolate          (regression)
  - PropertyValueVectorFloat_MultidirectionalConstant         (no regression)
  - PropertyValueVectorFloat_SingleNumberInterpolate_Evaluates(eval smoke)
  - PropertyValueVectorColor_SingleColorInterpolate_Evaluates (eval smoke,
                                                               added here to
                                                               cover the prior
                                                               commit's claim)

  All 7 PropertyValue* tests pass. Broader 75 in StyleConversion.*,
  Style*.*, StyleParser/*, Expression* also continue to pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Changes that affect the C++ core of MapLibre Native

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants