@@ -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]
300301const 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+
302306export 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 ) ;
0 commit comments