diff --git a/src/render/image_atlas.ts b/src/render/image_atlas.ts index ca1162a6605..0fb11e84247 100644 --- a/src/render/image_atlas.ts +++ b/src/render/image_atlas.ts @@ -161,7 +161,7 @@ export function sortImagesMap( if (a.variant.sx !== b.variant.sx) return a.variant.sx - b.variant.sx; if (a.variant.sy !== b.variant.sy) return a.variant.sy - b.variant.sy; - return 0; + return a.key.localeCompare(b.key); }); const sorted = new Map(); diff --git a/test/unit/render/image_atlas.test.ts b/test/unit/render/image_atlas.test.ts index 00e7447ad7d..b99e85d4049 100644 --- a/test/unit/render/image_atlas.test.ts +++ b/test/unit/render/image_atlas.test.ts @@ -75,6 +75,26 @@ describe('sortImagesMap', () => { expect(keys[2]).toEqual(icon2); }); + test('orders variants with same name and scale but different color params deterministically (GLJS #13689)', () => { + // Parametric SVG icons tinted per-feature share the same name and scale but + // differ only in color params. The packing order must not depend on Map + // insertion order, otherwise two atlases with identical content hashes can + // end up with different pixel layouts and getOrCache swaps tinted icons. + const green = new ImageVariant('circle-glow', {params: {fill: new Color(0.13, 0.77, 0.37, 1)}, sx: 0.7, sy: 0.7}).toString(); + const gray = new ImageVariant('circle-glow', {params: {fill: new Color(0.5, 0.5, 0.5, 1)}, sx: 0.7, sy: 0.7}).toString(); + + const greenFirst = sortImagesMap(new Map([ + [green, createMockImage('circle-glow')], + [gray, createMockImage('circle-glow')] + ])); + const grayFirst = sortImagesMap(new Map([ + [gray, createMockImage('circle-glow')], + [green, createMockImage('circle-glow')] + ])); + + expect(Array.from(greenFirst.keys())).toEqual(Array.from(grayFirst.keys())); + }); + test('populates variant cache when provided', () => { const iconId = createImageVariantId('icon', 1, 1); const markerId = createImageVariantId('marker', 2, 2);