Skip to content

Commit 289b965

Browse files
ibesoragithub-actions[bot]
authored andcommitted
Fix appearance text-color and icon-image cross-fade
GitOrigin-RevId: 513541cd4c665e7463fc02888ae1595ec7fcf991
1 parent 7be796d commit 289b965

4 files changed

Lines changed: 96 additions & 28 deletions

File tree

src/data/bucket/symbol_bucket.ts

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export type AppearanceFeatureData = {
107107
// Optional because these are only ever assigned on the main thread; the
108108
// worker doesn't need to serialize empty placeholders.
109109
layoutBasedIconVertexData?: Uint16Array;
110+
layoutBasedIconTransitioningVertexData?: Uint16Array;
110111
layoutBasedTextVertexData?: Uint16Array;
111112

112113
// Mutable render state — updated every frame by updateAppearances() on the main thread.
@@ -299,6 +300,9 @@ function containsRTLText(formattedText: Formatted): boolean {
299300
// Layout: [tileAnchorX, tileAnchorY, ox, oy, tx, ty, aSizeX, aSizeY, pixelOffsetX, pixelOffsetY, minFontScaleX, minFontScaleY]
300301
const SYMBOL_VERTEX_STRIDE = 12;
301302

303+
// Number of uint16 values per vertex in SymbolIconTransitioningArray (a_texb: [tx, ty]).
304+
const ICON_TRANSITIONING_STRIDE = 2;
305+
302306
export class SymbolBuffers {
303307
layoutVertexArray: SymbolLayoutArray;
304308
layoutVertexBuffer: VertexBuffer;
@@ -431,6 +435,15 @@ export class SymbolBuffers {
431435
this.layoutVertexArray.uint16.set(snapshot, offset * SYMBOL_VERTEX_STRIDE);
432436
}
433437

438+
snapshotIconTransitioningVertexData(offset: number, numVertices: number): Uint16Array {
439+
const start = offset * ICON_TRANSITIONING_STRIDE;
440+
return this.iconTransitioningVertexArray.uint16.slice(start, start + numVertices * ICON_TRANSITIONING_STRIDE);
441+
}
442+
443+
restoreIconTransitioningVertexData(offset: number, snapshot: Uint16Array) {
444+
this.iconTransitioningVertexArray.uint16.set(snapshot, offset * ICON_TRANSITIONING_STRIDE);
445+
}
446+
434447
updateSymbolVertexData(vertexIndex: number, anchorX: number, anchorY: number, newOx: number, newOy: number, newTx: number, newTy: number, newSizeX: number, newSizeY: number, pixelOffsetX: number, pixelOffsetY: number, minFontScaleX: number, minFontScaleY: number) {
435448
assert(vertexIndex >= 0 && vertexIndex < this.layoutVertexArray.length, 'Invalid vertex start index');
436449

@@ -1054,9 +1067,13 @@ class SymbolBucket implements Bucket {
10541067
appearances.forEach(a => {
10551068
const iconImage = a.getLayoutProperty('icon-image');
10561069
if (!iconImage) return;
1057-
const iconPrimary = this.getCombinedIconPrimary(a, symbolLayer, evaluationFeature, canonical, availableImages, symbolFeature, iconScaleFactor);
1070+
const {iconPrimary, iconSecondary} = this.getCombinedIconVariants(a, symbolLayer, evaluationFeature, canonical, availableImages, symbolFeature, iconScaleFactor);
10581071
if (!iconPrimary) return;
10591072
addImageVariantToIcons(iconPrimary);
1073+
if (iconSecondary) {
1074+
this.hasAnySecondaryIcon = true;
1075+
addImageVariantToIcons(iconSecondary);
1076+
}
10601077
});
10611078
}
10621079

@@ -1107,11 +1124,10 @@ class SymbolBucket implements Bucket {
11071124
}
11081125
}
11091126

1110-
getCombinedIconPrimary(appearance: SymbolAppearance, layer: SymbolStyleLayer, evaluationFeature: EvaluationFeature, canonical: CanonicalTileID, availableImages: ImageId[],
1127+
getCombinedIconVariants(appearance: SymbolAppearance, layer: SymbolStyleLayer, evaluationFeature: EvaluationFeature, canonical: CanonicalTileID, availableImages: ImageId[],
11111128
symbolFeature: SymbolFeature, iconScaleFactor: number
1112-
): ImageVariant | undefined {
1129+
): {iconPrimary: ImageVariant | undefined; iconSecondary: ImageVariant | undefined} {
11131130
let icon: ResolvedImage;
1114-
let iconPrimary: ImageVariant;
11151131
if (appearance.hasLayoutProperty('icon-image')) {
11161132
const resolvedTokens = layer.getAppearanceValueAndResolveTokens(appearance, 'icon-image', evaluationFeature, canonical, availableImages);
11171133
icon = this.getResolvedImageFromTokens(resolvedTokens as string);
@@ -1125,11 +1141,17 @@ class SymbolBucket implements Bucket {
11251141
appearance.getUnevaluatedLayoutProperty('icon-size') :
11261142
layer._unevaluatedLayout._values['icon-size']) as PropertyValue<number, PossiblyEvaluatedPropertyValue<number>>;
11271143
const iconSizeData = getSizeData(this.zoom, unevaluatedIconSize, this.worldview, availableImages);
1128-
const imageVariant = getScaledImageVariant(icon, iconSizeData, unevaluatedIconSize, canonical, this.zoom, symbolFeature, this.pixelRatio, iconScaleFactor, this.worldview, availableImages);
1129-
iconPrimary = imageVariant.iconPrimary;
1144+
const {iconPrimary, iconSecondary} = getScaledImageVariant(icon, iconSizeData, unevaluatedIconSize, canonical, this.zoom, symbolFeature, this.pixelRatio, iconScaleFactor, this.worldview, availableImages);
1145+
return {iconPrimary, iconSecondary};
11301146
}
11311147

1132-
return iconPrimary;
1148+
return {iconPrimary: undefined, iconSecondary: undefined};
1149+
}
1150+
1151+
getCombinedIconPrimary(appearance: SymbolAppearance, layer: SymbolStyleLayer, evaluationFeature: EvaluationFeature, canonical: CanonicalTileID, availableImages: ImageId[],
1152+
symbolFeature: SymbolFeature, iconScaleFactor: number
1153+
): ImageVariant | undefined {
1154+
return this.getCombinedIconVariants(appearance, layer, evaluationFeature, canonical, availableImages, symbolFeature, iconScaleFactor).iconPrimary;
11331155
}
11341156

11351157
private updateSymbolInstanceIconVertices(
@@ -1163,7 +1185,7 @@ class SymbolBucket implements Bucket {
11631185
id: featureData.id
11641186
};
11651187

1166-
const iconPrimary = this.getCombinedIconPrimary(activeAppearance, layer, evaluationFeature, canonical, availableImages, minimalFeature, iconScaleFactor);
1188+
const {iconPrimary, iconSecondary} = this.getCombinedIconVariants(activeAppearance, layer, evaluationFeature, canonical, availableImages, minimalFeature, iconScaleFactor);
11671189
if (!iconPrimary) return {vertexOffsetDelta: 0, hasChanges: false};
11681190

11691191
const primaryImageSerialized = iconPrimary.toString();
@@ -1173,7 +1195,10 @@ class SymbolBucket implements Bucket {
11731195
// Get values from appearance and fallback to layout ones
11741196
const {appearanceIconOffset: iconOffset, appearanceIconRotate: iconRotate} = getAppearanceIconValues(activeAppearance, layer, evaluationFeature as SymbolFeature, canonical, layoutIconOffset, layoutIconRotate, layoutIconSize, iconScaleFactor);
11751197
const iconAnchor = layer.layout.get('icon-anchor').evaluate(evaluationFeature, featureState, canonical);
1176-
let shapedIcon = shapeIcon(position, undefined, iconOffset, iconAnchor);
1198+
1199+
// Resolve secondary position for cross-fade support
1200+
const secondaryPosition = iconSecondary ? (this.iconAtlasPositions && this.iconAtlasPositions.get(iconSecondary.toString())) : undefined;
1201+
let shapedIcon = shapeIcon(position, secondaryPosition, iconOffset, iconAnchor);
11771202

11781203
const isSDFIcon = position.sdf;
11791204
// Get icon-text-fit from layout since we don't support it in appearances
@@ -1201,17 +1226,23 @@ class SymbolBucket implements Bucket {
12011226
// Generate new icon quads with updated texture coordinates and size
12021227
const iconQuads = getIconQuads(shapedIcon, iconRotate, isSDFIcon, iconTextFit !== 'none', iconScaleFactor);
12031228

1229+
const hasTransitioning = this.icon.iconTransitioningVertexArray.length > 0;
1230+
12041231
// Store the layout-based vertex data to restore it later if it's the first time we use an appearance for this feature
12051232
if (!featureData.isUsingAppearanceIconVertexData) {
12061233
featureData.isUsingAppearanceIconVertexData = true;
12071234
featureData.layoutBasedIconVertexData = this.icon.snapshotSymbolVertexData(vertexOffset, symbolInstance.numIconVertices);
1235+
if (hasTransitioning) {
1236+
featureData.layoutBasedIconTransitioningVertexData = this.icon.snapshotIconTransitioningVertexData(vertexOffset, symbolInstance.numIconVertices);
1237+
}
12081238
}
12091239

12101240
// Update vertex data - only update as many quads as were allocated during layout
12111241
const maxQuads = Math.floor(symbolInstance.numIconVertices / 4);
12121242
const quadsToUpdate = Math.min(iconQuads.length, maxQuads);
12131243

12141244
let currentVertexOffset = vertexOffset;
1245+
const transitioningUint16 = hasTransitioning ? this.icon.iconTransitioningVertexArray.uint16 : null;
12151246
for (let j = 0; j < quadsToUpdate; ++j) {
12161247
const quad = iconQuads[j];
12171248

@@ -1231,6 +1262,21 @@ class SymbolBucket implements Bucket {
12311262
this.icon.updateSymbolVertexData(currentVertexOffset + 1, anchorX, anchorY, Math.round(quad.tr.x * 32), Math.round(quad.tr.y * 32), quad.texPrimary.x + quad.texPrimary.w, quad.texPrimary.y, newSizeX, newSizeY, pixelOffsetBRX, pixelOffsetTLY, minFontScaleX, minFontScaleY);
12321263
this.icon.updateSymbolVertexData(currentVertexOffset + 2, anchorX, anchorY, Math.round(quad.bl.x * 32), Math.round(quad.bl.y * 32), quad.texPrimary.x, quad.texPrimary.y + quad.texPrimary.h, newSizeX, newSizeY, pixelOffsetTLX, pixelOffsetBRY, minFontScaleX, minFontScaleY);
12331264
this.icon.updateSymbolVertexData(currentVertexOffset + 3, anchorX, anchorY, Math.round(quad.br.x * 32), Math.round(quad.br.y * 32), quad.texPrimary.x + quad.texPrimary.w, quad.texPrimary.y + quad.texPrimary.h, newSizeX, newSizeY, pixelOffsetBRX, pixelOffsetBRY, minFontScaleX, minFontScaleY);
1265+
1266+
// Update secondary (cross-fade) texture coords: fall back to primary when no secondary.
1267+
if (transitioningUint16) {
1268+
const tex = quad.texSecondary ? quad.texSecondary : quad.texPrimary;
1269+
const base = currentVertexOffset * ICON_TRANSITIONING_STRIDE;
1270+
transitioningUint16[base] = tex.x;
1271+
transitioningUint16[base + 1] = tex.y;
1272+
transitioningUint16[base + 2] = tex.x + tex.w;
1273+
transitioningUint16[base + 3] = tex.y;
1274+
transitioningUint16[base + 4] = tex.x;
1275+
transitioningUint16[base + 5] = tex.y + tex.h;
1276+
transitioningUint16[base + 6] = tex.x + tex.w;
1277+
transitioningUint16[base + 7] = tex.y + tex.h;
1278+
}
1279+
12341280
currentVertexOffset += 4;
12351281
}
12361282

@@ -1255,6 +1301,9 @@ class SymbolBucket implements Bucket {
12551301
} else if (featureData.isUsingAppearanceIconVertexData) {
12561302
// No active appearance but vertex buffer still holds appearance data — restore original layout vertices.
12571303
this.icon.restoreSymbolVertexData(vertexOffset, featureData.layoutBasedIconVertexData);
1304+
if (featureData.layoutBasedIconTransitioningVertexData) {
1305+
this.icon.restoreIconTransitioningVertexData(vertexOffset, featureData.layoutBasedIconTransitioningVertexData);
1306+
}
12581307
featureData.isUsingAppearanceIconVertexData = false;
12591308
return {vertexOffsetDelta: featureData.layoutBasedIconVertexData.length / SYMBOL_VERTEX_STRIDE, hasChanges: true};
12601309
}
@@ -1671,6 +1720,12 @@ class SymbolBucket implements Bucket {
16711720
}
16721721
}
16731722

1723+
if (hasIconChanges && this.icon.iconTransitioningVertexBuffer && this.icon.iconTransitioningVertexArray.length > 0) {
1724+
if (this.icon.iconTransitioningVertexArray.length === this.icon.iconTransitioningVertexBuffer.length) {
1725+
this.icon.iconTransitioningVertexBuffer.updateData(this.icon.iconTransitioningVertexArray);
1726+
}
1727+
}
1728+
16741729
if (hasTextChanges && this.text.layoutVertexBuffer && this.text.layoutVertexArray.arrayBuffer !== null) {
16751730
if (this.text.layoutVertexArray.length === this.text.layoutVertexBuffer.length) {
16761731
this.text.layoutVertexBuffer.updateData(this.text.layoutVertexArray);

src/data/bucket/symbol_property_binder_ubo.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,12 @@ export class SymbolPropertyBinderUBO {
132132
maxUniformBufferBindings: number;
133133

134134
// Per-feature tracking, in insertion (populate) order. One entry per populateUBO call;
135-
// the entry's index IS the feature's global index (see _writeFeatureBlock), so batch/local
136-
// need not be stored. These two parallel arrays are the only feature-tracking state
137-
// transferred worker→main; the lookup maps below are rebuilt lazily on the main thread.
135+
// the entry's index IS the feature's global index (see _writeFeatureBlock).
136+
// These parallel arrays are the only feature-tracking state transferred worker→main,
137+
// the lookup maps below are rebuilt lazily on the main thread.
138138
allFeatureVtIndices: number[]; // vector-tile feature index per entry
139139
allFeatureIds: Array<string | number | undefined>; // feature id per entry (if any)
140+
allFormattedSections: Array<FormattedSection | null>; // formatted section per entry (for per-section color overrides)
140141

141142
// Lazily built on the main thread (omitted from serialization). Map a featureId / vtFeatureIndex
142143
// to the positions in allFeature* that reference it, for O(1) targeted updates.
@@ -196,6 +197,7 @@ export class SymbolPropertyBinderUBO {
196197

197198
this.allFeatureVtIndices = [];
198199
this.allFeatureIds = [];
200+
this.allFormattedSections = [];
199201
this.featureVertexRangesFromId = null;
200202
this.featureVertexRangesFromVtIndex = null;
201203
this.ubos = [];
@@ -343,14 +345,24 @@ export class SymbolPropertyBinderUBO {
343345
/**
344346
* Resolve a paint property by name, preferring the active appearance's override when it
345347
* defines that property, otherwise the layer's paint. Shared by all three evaluate paths.
348+
*
349+
* When formattedSection is provided and the layer's property has a section override for it
350+
* (e.g. per-section text-color in a formatted text-field), the section's explicit value
351+
* takes precedence over the appearance. Return the layer's prop so its FormatSectionOverride
352+
* correctly returns the section color.
346353
*/
347-
private _resolveProp<T>(propName: string, activeAppearance: SymbolAppearance | null | undefined): PossiblyEvaluatedPropertyValue<T> | undefined {
354+
private _resolveProp<T>(propName: string, activeAppearance: SymbolAppearance | null | undefined,
355+
formattedSection?: FormattedSection): PossiblyEvaluatedPropertyValue<T> | undefined {
356+
const paint = this.layer.paint;
357+
const layerProp = paint.get(propName as keyof typeof paint._values) as unknown as PossiblyEvaluatedPropertyValue<T>;
348358
const appearanceName = propName as keyof AppearancePaintProps;
349359
if (activeAppearance && activeAppearance.hasPaintProperty(appearanceName)) {
360+
if (formattedSection && layerProp && layerProp.property.overrides && layerProp.property.overrides.hasOverride(formattedSection)) {
361+
return layerProp;
362+
}
350363
return activeAppearance.paintProperties.get(appearanceName) as unknown as PossiblyEvaluatedPropertyValue<T> | undefined;
351364
}
352-
const paint = this.layer.paint;
353-
return paint.get(propName as keyof typeof paint._values) as unknown as PossiblyEvaluatedPropertyValue<T> | undefined;
365+
return layerProp;
354366
}
355367

356368
/** Evaluate a property at the given zoom params, with the verbose shared argument list filled in. */
@@ -372,7 +384,7 @@ export class SymbolPropertyBinderUBO {
372384
ctx: EvaluationContext,
373385
flatOffset: number
374386
): void {
375-
const prop = this._resolveProp<Color>(propName, ctx.activeAppearance);
387+
const prop = this._resolveProp<Color>(propName, ctx.activeAppearance, ctx.formattedSection);
376388

377389
if (!prop) {
378390
evalFlatScratch[flatOffset] = 0;
@@ -638,7 +650,8 @@ export class SymbolPropertyBinderUBO {
638650
};
639651

640652
const activeAppearance = this.activeAppearanceByVtIndex ? this.activeAppearanceByVtIndex.get(vtFeatureIndex) : undefined;
641-
const allValues = this.evaluateAllProperties(feature, featureState, canonical, availableImages, brightness, undefined, activeAppearance);
653+
const formattedSection = this.allFormattedSections ? this.allFormattedSections[i] : undefined;
654+
const allValues = this.evaluateAllProperties(feature, featureState, canonical, availableImages, brightness, formattedSection || undefined, activeAppearance);
642655
this._writeFeatureBlock(i, allValues);
643656
}
644657

@@ -723,6 +736,7 @@ export class SymbolPropertyBinderUBO {
723736
// _writeFeatureBlock re-derives batch/local; the lookup maps are built lazily on the main thread.
724737
this.allFeatureVtIndices.push(vtFeatureIndex);
725738
this.allFeatureIds.push(featureId);
739+
this.allFormattedSections.push(formattedSection || null);
726740

727741
return localIndex;
728742
}
@@ -806,11 +820,13 @@ export class SymbolPropertyBinderUBO {
806820
const positions = this.featureVertexRangesFromVtIndex.get(vtFeatureIndex);
807821
if (!positions) return false;
808822

809-
// All positions for this vtFeatureIndex evaluate identically (same feature/state/appearance),
810-
// so evaluate once and write the shared result into each slot.
811-
const allValues = this.evaluateAllProperties(feature, featureState, canonical, availableImages, brightness, undefined, activeAppearance);
823+
// Evaluate per slot: sections of a formatted text-field each have their own UBO entry
824+
// and may have per-section paint overrides (e.g. text-color from format expression).
825+
// Re-use the stored formattedSection so those overrides take precedence over the appearance.
812826
let wrote = false;
813827
for (const i of positions) {
828+
const formattedSection = this.allFormattedSections ? this.allFormattedSections[i] : undefined;
829+
const allValues = this.evaluateAllProperties(feature, featureState, canonical, availableImages, brightness, formattedSection || undefined, activeAppearance);
814830
wrote = this._writeFeatureBlock(i, allValues) || wrote;
815831
}
816832
return wrote;

src/symbol/symbol_layout.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,11 @@ export function postRasterizationSymbolLayout(bucket: SymbolBucket, bucketData:
872872
availableImages: ImageId[], canonical: CanonicalTileID, tileZoom: number, projection: Projection, brightness: number | null, imageMap: StyleImageMap<StringifiedImageVariant>, imageAtlas: {iconPositions: ImagePositionMap}) {
873873

874874
bucket.iconAtlasPositions = imageAtlas.iconPositions;
875-
const {featureData, hasAnySecondaryIcon, sizes, textAlongLine, symbolPlacement, coverageFrcMask, coveragePolygons, coverageTileZoom, symbolAnchorInFrcCoverage} = bucketData;
875+
const {featureData, sizes, textAlongLine, symbolPlacement, coverageFrcMask, coveragePolygons, coverageTileZoom, symbolAnchorInFrcCoverage} = bucketData;
876+
// An appearance may introduce a secondary icon even when the layout icon has
877+
// no secondary. Include bucket.hasAnySecondaryIcon so transitioning vertices are created and the
878+
// iconTransitioningVertexBuffer is populated for appearance-driven cross-fades.
879+
const hasAnySecondaryIcon = bucketData.hasAnySecondaryIcon || bucket.hasAnySecondaryIcon;
876880

877881
for (const data of featureData) {
878882
const {shapedIcon, verticallyShapedIcon, feature, shapedTextOrientations, shapedText, layoutTextSize, layoutIconSize,

test/ignores/all.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,6 @@ const skip = [
298298
"render-tests/hd-sd-transition/hd-on-remove-import",
299299
"render-tests/hd-sd-transition/sd-on-steady",
300300

301-
// https://mapbox.atlassian.net/browse/GLJS-1855
302-
"render-tests/appearance/text-field",
303-
304-
// https://mapbox.atlassian.net/browse/GLJS-1856
305-
"render-tests/appearance/icon-image-cross-fade/two-to-two",
306-
"render-tests/appearance/icon-image-cross-fade/one-to-two",
307-
"render-tests/appearance/icon-image-cross-fade/two-to-one"
308301
];
309302

310303
export default {todo, skip};

0 commit comments

Comments
 (0)