Skip to content

Commit 191bcff

Browse files
ibesoragithub-actions[bot]
authored andcommitted
Support icon-text-fit on apperances icons
GitOrigin-RevId: ec0abad8ca35bc6fdd79d92c8b5484a1f76965b0
1 parent 53525d6 commit 191bcff

File tree

7 files changed

+282
-48
lines changed

7 files changed

+282
-48
lines changed

src/data/bucket/symbol_bucket.ts

Lines changed: 113 additions & 40 deletions
Large diffs are not rendered by default.

src/symbol/quads.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {isVerticalClosePunctuation, isVerticalOpenPunctuation} from '../util/ver
66
import ONE_EM from './one_em';
77
import {warnOnce} from '../util/util';
88

9+
import type {ImagePosition} from '../render/image_atlas';
910
import type Anchor from './anchor';
1011
import type {PositionedIcon, Shaping} from './shaping';
1112
import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
@@ -200,6 +201,22 @@ export function getIconQuads(
200201
return quads;
201202
}
202203

204+
export function getIconQuadsNumber(image: ImagePosition, hasIconTextFit: boolean): number {
205+
const imageWidth = image.paddedRect.w - 2 * border;
206+
const imageHeight = image.paddedRect.h - 2 * border;
207+
208+
const stretchX = image.stretchX || [[0, imageWidth]];
209+
const stretchY = image.stretchY || [[0, imageHeight]];
210+
211+
if (!hasIconTextFit || (!image.stretchX && !image.stretchY)) {
212+
return 1;
213+
}
214+
215+
const xCuts = stretchZonesNumber(stretchX);
216+
const yCuts = stretchZonesNumber(stretchY);
217+
return xCuts * yCuts;
218+
}
219+
203220
function sumWithinRange(ranges: Array<[number, number]>, min: number, max: number) {
204221
let sum = 0;
205222
for (const range of ranges) {
@@ -229,6 +246,12 @@ function stretchZonesToCuts(stretchZones: Array<[number, number]>, fixedSize: nu
229246
return cuts;
230247
}
231248

249+
function stretchZonesNumber(stretchZones: Array<[number, number]>) {
250+
// We create two cuts per stretch zone plus an extra one
251+
// See stretchZonesToCuts
252+
return 2 * stretchZones.length + 1;
253+
}
254+
232255
function getEmOffset(stretchOffset: number, stretchSize: number, iconSize: number, iconOffset: number) {
233256
return stretchOffset / stretchSize * iconSize + iconOffset;
234257
}

src/symbol/symbol_layout.ts

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Anchor from './anchor';
22
import {getAnchors, getCenterAnchor} from './get_anchors';
33
import {shapeText, shapeIcon, WritingMode, fitIconToText, isPositionedIcon, getPositionedIconSize, isFullyStretchableX, isFullyStretchableY} from './shaping';
4-
import {getGlyphQuads, getIconQuads} from './quads';
4+
import {getGlyphQuads, getIconQuads, getIconQuadsNumber, type SymbolQuad} from './quads';
55
import {warnOnce, degToRad, clamp} from '../util/util';
66
import {
77
allowsVerticalWritingMode,
@@ -857,11 +857,23 @@ function addFeature(bucket: SymbolBucket,
857857
}
858858
const layout = bucket.layers[0].layout;
859859

860+
const glyphSize = ONE_EM;
861+
const fontScale = layoutTextSize * sizes.textScaleFactor / glyphSize;
862+
860863
const defaultShaping = getDefaultHorizontalShaping(shapedTextOrientations.horizontal) || shapedTextOrientations.vertical;
864+
865+
// Store text shaping data for icon-text-fit appearance updates
866+
if (iconTextFit !== 'none' && bucket.appearanceFeatureData && feature.index < bucket.appearanceFeatureData.length) {
867+
const featureData = bucket.appearanceFeatureData[feature.index];
868+
if (featureData) {
869+
featureData.textShaping = defaultShaping;
870+
featureData.iconTextFitPadding = layout.get('icon-text-fit-padding').evaluate(feature, {}, canonical);
871+
featureData.fontScale = fontScale;
872+
}
873+
}
861874
const isGlobe = projection.name === 'globe';
862875

863-
const glyphSize = ONE_EM,
864-
textMaxBoxScale = bucket.tilePixelRatio * textMaxSize / glyphSize,
876+
const textMaxBoxScale = bucket.tilePixelRatio * textMaxSize / glyphSize,
865877
iconBoxScale = bucket.tilePixelRatio * layoutIconSize,
866878
symbolMinDistance = tilePixelRatioForSymbolSpacing(bucket.overscaling, bucket.zoom) * layout.get('symbol-spacing'),
867879
textPadding = layout.get('text-padding') * bucket.tilePixelRatio,
@@ -1036,7 +1048,8 @@ function addTextVertices(bucket: SymbolBucket,
10361048
canonical,
10371049
brightness,
10381050
false,
1039-
symbolInstanceIndex);
1051+
symbolInstanceIndex,
1052+
glyphQuads.length);
10401053

10411054
// The placedSymbolArray is used at render time in drawTileSymbols
10421055
// These indices allow access to the array at collision detection time
@@ -1229,7 +1242,12 @@ function addSymbol(bucket: SymbolBucket,
12291242
const iconQuads = getIconQuads(shapedIcon, iconRotate, isSDFIcon, hasIconTextFit, sizes.iconScaleFactor);
12301243
const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate, isSDFIcon, hasIconTextFit, sizes.iconScaleFactor) : undefined;
12311244
iconBoxIndex = evaluateBoxCollisionFeature(collisionBoxArray, collisionFeatureAnchor, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconPadding, iconRotate, null, iconCollisionBounds);
1232-
numIconVertices = iconQuads.length * 4;
1245+
// Calculate maximum quads needed across layout icon and all appearance variants
1246+
// to prevent vertex buffer overflow during appearance updates
1247+
const maxQuadCount = calculateMaxIconQuadCount(bucket, iconQuads, verticalIconQuads,
1248+
layer.layout, feature, canonical, bucket.iconAtlasPositions,
1249+
hasIconTextFit);
1250+
numIconVertices = maxQuadCount * 4;
12331251

12341252
let iconSizeData = null;
12351253

@@ -1271,12 +1289,13 @@ function addSymbol(bucket: SymbolBucket,
12711289
canonical,
12721290
brightness,
12731291
hasAnySecondaryIcon,
1274-
bucket.symbolInstances.length);
1292+
bucket.symbolInstances.length,
1293+
maxQuadCount);
12751294

12761295
placedIconSymbolIndex = bucket.icon.placedSymbolArray.length - 1;
12771296

12781297
if (verticalIconQuads) {
1279-
numVerticalIconVertices = verticalIconQuads.length * 4;
1298+
numVerticalIconVertices = maxQuadCount * 4;
12801299

12811300
bucket.addSymbols(
12821301
bucket.icon,
@@ -1297,7 +1316,8 @@ function addSymbol(bucket: SymbolBucket,
12971316
canonical,
12981317
brightness,
12991318
hasAnySecondaryIcon,
1300-
bucket.symbolInstances.length);
1319+
bucket.symbolInstances.length,
1320+
maxQuadCount);
13011321

13021322
verticalPlacedIconSymbolIndex = bucket.icon.placedSymbolArray.length - 1;
13031323
}
@@ -1428,3 +1448,58 @@ function anchorIsTooClose(bucket: SymbolBucket, text: string, repeatDistance: nu
14281448
compareText[text].push(anchor);
14291449
return false;
14301450
}
1451+
1452+
function calculateMaxIconQuadCount(
1453+
bucket: SymbolBucket,
1454+
iconQuads: Array<SymbolQuad>,
1455+
verticalIconQuads: Array<SymbolQuad> | undefined,
1456+
layout: PossiblyEvaluated<LayoutProps>,
1457+
feature: SymbolFeature,
1458+
canonical: CanonicalTileID,
1459+
imagePositions: ImagePositionMap,
1460+
hasIconTextFit: boolean,
1461+
): number {
1462+
const symbolLayer = bucket.layers[0];
1463+
const appearances = symbolLayer.appearances;
1464+
1465+
// Start with the layout icon quad count
1466+
let maxQuadCount = iconQuads.length;
1467+
1468+
// Check vertical icon quads if they exist
1469+
if (verticalIconQuads) {
1470+
maxQuadCount = Math.max(maxQuadCount, verticalIconQuads.length);
1471+
}
1472+
1473+
if (appearances.length === 0) {
1474+
return maxQuadCount;
1475+
}
1476+
1477+
const [iconSizeScaleRangeMin, iconSizeScaleRangeMax] = layout.get('icon-size-scale-range');
1478+
const iconScaleFactor = clamp(1, iconSizeScaleRangeMin, iconSizeScaleRangeMax);
1479+
1480+
// Check each appearance that has an icon to find maximum quad count needed
1481+
for (const appearance of appearances) {
1482+
const unevaluatedProperties = appearance.getUnevaluatedProperties();
1483+
const iconImageProperty = unevaluatedProperties._values['icon-image'].value !== undefined;
1484+
1485+
if (iconImageProperty) {
1486+
const appearanceIconImage = symbolLayer.getAppearanceValueAndResolveTokens(appearance, 'icon-image', feature, canonical, []);
1487+
if (appearanceIconImage) {
1488+
const icon = bucket.getResolvedImageFromTokens(appearanceIconImage as string);
1489+
if (icon) {
1490+
// Ideally we shouldn't need to compute the scaled image variant because all
1491+
// different sized versions of the same icon have the same number of stretchable
1492+
// areas, which is what we need but unfortunately, since imagePositions stores the
1493+
// position by the stringified sized icon we need to compute it
1494+
const unevaluatedIconSize = unevaluatedProperties._values['icon-size'];
1495+
const iconSizeData = getSizeData(bucket.zoom, unevaluatedIconSize, bucket.worldview);
1496+
const imageVariant = getScaledImageVariant(icon, iconSizeData, unevaluatedIconSize, canonical, bucket.zoom, feature, bucket.pixelRatio, iconScaleFactor, bucket.worldview);
1497+
const imagePosition = imagePositions.get(imageVariant.iconPrimary.toString());
1498+
maxQuadCount = Math.max(maxQuadCount, getIconQuadsNumber(imagePosition, hasIconTextFit));
1499+
}
1500+
}
1501+
}
1502+
}
1503+
1504+
return maxQuadCount;
1505+
}
3.21 KB
Loading
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"version": 8,
3+
"metadata": {
4+
"test": {
5+
"width": 200,
6+
"height": 150,
7+
"operations": [
8+
["wait"],
9+
["setZoom", 1],
10+
["wait"]
11+
]
12+
}
13+
},
14+
"zoom": 0.5,
15+
"sources": {
16+
"geojson": {
17+
"type": "geojson",
18+
"data": {
19+
"type": "FeatureCollection",
20+
"features": [{
21+
"type": "Feature",
22+
"properties": {
23+
"anchor": "center"
24+
},
25+
"geometry": {
26+
"type": "Point",
27+
"coordinates": [ 0, 0 ]
28+
}
29+
}]
30+
}
31+
}
32+
},
33+
"sprite": "local://sprites/stretch",
34+
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
35+
"layers": [
36+
{
37+
"id": "icons",
38+
"type": "symbol",
39+
"source": "geojson",
40+
"layout": {
41+
"text-field": "ASDF",
42+
"text-size": 20,
43+
"text-anchor": "center",
44+
"text-font": [ "Open Sans Semibold", "Arial Unicode MS Bold" ],
45+
"text-allow-overlap": true,
46+
"text-ignore-placement": true,
47+
"icon-image": "nine-part",
48+
"icon-text-fit": "both",
49+
"icon-allow-overlap": true,
50+
"icon-ignore-placement": true
51+
},
52+
"appearances": [
53+
{
54+
"name": "test",
55+
"condition": [">=", ["zoom"], 1],
56+
"properties": {
57+
"icon-image": "fifteen-part"
58+
}
59+
}
60+
]
61+
}
62+
]
63+
}

0 commit comments

Comments
 (0)