Skip to content

Commit c8816cc

Browse files
ibesoragithub-actions[bot]
authored andcommitted
Add support for text-rotate, text-size and text-offset to appearances (internal-7731)
GitOrigin-RevId: 213415f3bab96d41f09d8cc4d047369f11ac83e2
1 parent 099ed13 commit c8816cc

File tree

43 files changed

+615
-391
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+615
-391
lines changed

src/data/bucket.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {ElevationFeature} from '../../3d-style/elevation/elevation_feature'
2222
import type {ImageId, StringifiedImageId} from '../style-spec/expression/types/image_id';
2323
import type {StyleModelMap} from '../style/style_mode';
2424
import type {GlobalProperties} from '../style-spec/expression';
25+
import type ImageManager from '../render/image_manager';
2526

2627
export type BucketParameters<Layer extends TypedStyleLayer> = {
2728
index: number;
@@ -137,7 +138,7 @@ export interface Bucket {
137138
*/
138139
destroy: (reload?: boolean) => void;
139140
updateFootprints: (id: UnwrappedTileID, footprints: Array<TileFootprint>) => void;
140-
updateAppearances: (canonical?: CanonicalTileID, featureState?: FeatureStates, availableImages?: Array<ImageId>, globalProperties?: GlobalProperties) => void;
141+
updateAppearances: (canonical?: CanonicalTileID, featureState?: FeatureStates, availableImages?: Array<ImageId>, globalProperties?: GlobalProperties, imageManager?: ImageManager) => void;
141142
}
142143

143144
export function deserialize(input: Array<Bucket>, style: Style): Record<string, Bucket> {

src/data/bucket/symbol_bucket.ts

Lines changed: 412 additions & 178 deletions
Large diffs are not rendered by default.

src/shaders/symbol.vertex.glsl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#include "_prelude_terrain.vertex.glsl"
22
#include "_prelude_shadow.vertex.glsl"
33

4-
#define APPEARANCE_ICON 1.0
4+
#define USING_APPEARANCE 1.0
55

66
in vec4 a_pos_offset;
77
in vec4 a_tex_size;
@@ -125,15 +125,15 @@ void main() {
125125

126126
float a_size_min = floor(a_size[0] * 0.5);
127127
float a_size_max = floor(a_size[1] * 0.5);
128-
float a_apperance_icon = a_size[1] - 2.0 * a_size_max;
128+
float a_apperance = a_size[1] - 2.0 * a_size_max;
129129
vec2 a_pxoffset = a_pixeloffset.xy;
130130
vec2 a_min_font_scale = a_pixeloffset.zw / 256.0;
131131

132132
highp float segment_angle = -a_projected_pos[3];
133133
float size;
134134

135-
// When rendering icons for appearances, we use a_size_max to store the icon size
136-
if (a_apperance_icon == APPEARANCE_ICON) {
135+
// When rendering appearances, we use a_size_max to store the size
136+
if (a_apperance == USING_APPEARANCE) {
137137
size = a_size_max / 128.0;
138138
} else if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {
139139
size = mix(a_size_min, a_size_max, u_size_t) / 128.0;

src/source/tile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ class Tile {
726726
brightness: painter.style.getBrightness() || 0,
727727
worldview: painter.worldview
728728
};
729-
bucket.updateAppearances(this.tileID.canonical, sourceLayerStates, availableImages, globalProperties);
729+
bucket.updateAppearances(this.tileID.canonical, sourceLayerStates, availableImages, globalProperties, painter.imageManager);
730730
}
731731
if (bucket instanceof LineBucket || bucket instanceof FillBucket) {
732732
if (painter._terrain && painter._terrain.enabled && sourceCache && bucket.uploadPending()) {

src/symbol/quads.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,14 @@ export function getGlyphQuads(
296296
feature: Feature,
297297
imageMap: StyleImageMap<StringifiedImageVariant>,
298298
allowVerticalPlacement: boolean,
299+
textRotate?: number,
299300
): Array<SymbolQuad> {
300301
const quads = [];
301302
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
302303
if (shaping.positionedLines.length === 0) return quads;
303304

304-
const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}) * Math.PI / 180;
305+
const textRotateValue = textRotate !== undefined ? textRotate : layer.layout.get('text-rotate').evaluate(feature, {});
306+
const textRotateRadians = textRotateValue * Math.PI / 180;
305307
const rotateOffset = getRotateOffset(textOffset);
306308

307309
let shapingHeight = Math.abs(shaping.top - shaping.bottom);
@@ -467,7 +469,7 @@ export function getGlyphQuads(
467469
br = new Point(tl.x + paddedHeight, tl.y - paddedWidth);
468470
}
469471

470-
if (textRotate) {
472+
if (textRotateRadians) {
471473
let center: Point;
472474
if (!alongLine) {
473475
if (useRotateOffset) {
@@ -478,10 +480,10 @@ export function getGlyphQuads(
478480
} else {
479481
center = new Point(0, 0);
480482
}
481-
tl._rotateAround(textRotate, center);
482-
tr._rotateAround(textRotate, center);
483-
bl._rotateAround(textRotate, center);
484-
br._rotateAround(textRotate, center);
483+
tl._rotateAround(textRotateRadians, center);
484+
tr._rotateAround(textRotateRadians, center);
485+
bl._rotateAround(textRotateRadians, center);
486+
br._rotateAround(textRotateRadians, center);
485487
}
486488

487489
const pixelOffsetTL = new Point(0, 0);

src/symbol/symbol_layout.ts

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ function updateIconBoundingBoxes(input : {iconBBox: SymbolBoundingBox | null, ic
638638
iconOffset: [number, number], baseIconRotate: number, layoutIconSize: number, sizes: Sizes, shapedIcon: PositionedIcon, imagePositions: ImagePositionMap, iconScaleFactor: number,
639639
iconAnchor: SymbolAnchor) {
640640

641-
const {appearanceIconOffset, appearanceIconRotate, appearanceIconSize} = getAppearanceIconValues(appearance, symbolLayer, feature, canonical, iconOffset, baseIconRotate, layoutIconSize, sizes);
641+
const {appearanceIconOffset, appearanceIconRotate, appearanceIconSize} = getAppearanceIconValues(appearance, symbolLayer, feature, canonical, iconOffset, baseIconRotate, layoutIconSize, sizes.iconScaleFactor);
642642

643643
let appearanceShapedIcon: PositionedIcon | null = null;
644644
let appearanceVerticallyShapedIcon: PositionedIcon | null = null;
@@ -667,8 +667,8 @@ function updateIconBoundingBoxes(input : {iconBBox: SymbolBoundingBox | null, ic
667667
}
668668
}
669669

670-
function getAppearanceIconValues(appearance: SymbolAppearance, symbolLayer: SymbolStyleLayer, feature: SymbolFeature,
671-
canonical: CanonicalTileID, iconOffset: [number, number], baseIconRotate: number, layoutIconSize: number, sizes: Sizes) {
670+
export function getAppearanceIconValues(appearance: SymbolAppearance, symbolLayer: SymbolStyleLayer, feature: SymbolFeature,
671+
canonical: CanonicalTileID, iconOffset: [number, number], baseIconRotate: number, layoutIconSize: number, iconScaleFactor: number) {
672672
const appearanceIconOffsetValue = appearance.hasProperty('icon-offset') ?
673673
symbolLayer.getAppearanceValueAndResolveTokens(appearance, 'icon-offset', feature, canonical, []) :
674674
null;
@@ -687,7 +687,7 @@ function getAppearanceIconValues(appearance: SymbolAppearance, symbolLayer: Symb
687687
symbolLayer.getAppearanceValueAndResolveTokens(appearance, 'icon-size', feature, canonical, []) :
688688
null;
689689
const appearanceIconSize = (typeof appearanceIconSizeValue === 'number') ?
690-
appearanceIconSizeValue * sizes.iconScaleFactor :
690+
appearanceIconSizeValue * iconScaleFactor :
691691
layoutIconSize;
692692

693693
return {appearanceIconOffset, appearanceIconRotate, appearanceIconSize};
@@ -724,7 +724,7 @@ function updateTextBoundingBoxes(input: {textBBox: SymbolBoundingBox | null, tex
724724
}
725725
}
726726

727-
function getAppearanceTextValues(appearance: SymbolAppearance, symbolLayer: SymbolStyleLayer, feature: SymbolFeature,
727+
export function getAppearanceTextValues(appearance: SymbolAppearance, symbolLayer: SymbolStyleLayer, feature: SymbolFeature,
728728
canonical: CanonicalTileID, textOffset: [number, number], baseTextRotate: number, layoutTextSize: number) {
729729
const appearanceTextOffsetValue = appearance.hasProperty('text-offset') ?
730730
symbolLayer.getAppearanceValueAndResolveTokens(appearance, 'text-offset', feature, canonical, []) :
@@ -919,6 +919,12 @@ function fitIconsToText(bucket: SymbolBucket, shapedIcon: PositionedIcon | undef
919919
return {defaultShapedIcon, verticallyShapedIcon};
920920
}
921921

922+
export function computeFontScale(textSize: number, textScaleFactor: number) {
923+
const glyphSize = ONE_EM;
924+
const fontScale = textSize * textScaleFactor / glyphSize;
925+
return fontScale;
926+
}
927+
922928
/**
923929
* Given a feature and its shaped text and icon data, add a 'symbol
924930
* instance' for each _possible_ placement of the symbol feature.
@@ -963,17 +969,19 @@ function addFeature(bucket: SymbolBucket,
963969
const layout = bucket.layers[0].layout;
964970

965971
const glyphSize = ONE_EM;
966-
const fontScale = layoutTextSize * sizes.textScaleFactor / glyphSize;
972+
const fontScale = computeFontScale(layoutTextSize, sizes.textScaleFactor);
967973

968974
const defaultShaping = getDefaultHorizontalShaping(shapedTextOrientations.horizontal) || shapedTextOrientations.vertical;
969975

970-
// Store text shaping data for icon-text-fit appearance updates
971-
if (iconTextFit !== 'none' && bucket.appearanceFeatureData && feature.index < bucket.appearanceFeatureData.length) {
972-
const featureData = bucket.appearanceFeatureData[feature.index];
976+
// Store text shaping data for icon-text-fit and text appearance updates
977+
const hasTextAppearances = bucket.hasAnyAppearanceProperty(['text-size', 'text-offset', 'text-rotate']);
978+
if ((iconTextFit !== 'none' || hasTextAppearances) && bucket.appearanceFeatureData && bucket.featureToAppearanceIndex[feature.index] < bucket.appearanceFeatureData.length) {
979+
const featureData = bucket.appearanceFeatureData[bucket.featureToAppearanceIndex[feature.index]];
973980
if (featureData) {
974981
featureData.textShaping = defaultShaping;
975982
featureData.iconTextFitPadding = layout.get('icon-text-fit-padding').evaluate(feature, {}, canonical);
976983
featureData.fontScale = fontScale;
984+
featureData.textScaleFactor = sizes.textScaleFactor;
977985
}
978986
}
979987
const isGlobe = projection.name === 'globe';
@@ -1115,27 +1123,12 @@ function addTextVertices(bucket: SymbolBucket,
11151123
symbolInstanceIndex: number,
11161124
brightness?: number | null) {
11171125
const glyphQuads = getGlyphQuads(tileAnchor, shapedText, textOffset,
1118-
layer, textAlongLine, feature, imageMap, bucket.allowVerticalPlacement);
1126+
layer, textAlongLine, feature, imageMap, bucket.allowVerticalPlacement, undefined);
11191127

1120-
const sizeData = bucket.textSizeData;
1121-
let textSizeData: number[] = null;
1122-
1123-
if (sizeData.kind === 'source') {
1124-
textSizeData = [
1125-
SIZE_PACK_FACTOR * layer.layout.get('text-size').evaluate(feature, {}, canonical) * sizes.textScaleFactor
1126-
];
1127-
if (textSizeData[0] > MAX_PACKED_SIZE) {
1128-
warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`);
1129-
}
1130-
} else if (sizeData.kind === 'composite') {
1131-
textSizeData = [
1132-
SIZE_PACK_FACTOR * sizes.compositeTextSizes[0].evaluate(feature, {}, canonical) * sizes.textScaleFactor,
1133-
SIZE_PACK_FACTOR * sizes.compositeTextSizes[1].evaluate(feature, {}, canonical) * sizes.textScaleFactor
1134-
];
1135-
if (textSizeData[0] > MAX_PACKED_SIZE || textSizeData[1] > MAX_PACKED_SIZE) {
1136-
warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`);
1137-
}
1138-
}
1128+
const evaluatedTextSize = layer.layout.get('text-size').evaluate(feature, {}, canonical);
1129+
const minZoomSize = sizes.compositeTextSizes ? sizes.compositeTextSizes[0].evaluate(feature, {}, canonical) : 0;
1130+
const maxZoomSize = sizes.compositeTextSizes ? sizes.compositeTextSizes[1].evaluate(feature, {}, canonical) : 0;
1131+
const textSizeData = getSizeDataFromKind(bucket.layerIds[0], bucket.textSizeData, evaluatedTextSize, sizes.textScaleFactor, minZoomSize, maxZoomSize);
11391132

11401133
bucket.addSymbols(
11411134
bucket.text,
@@ -1166,6 +1159,32 @@ function addTextVertices(bucket: SymbolBucket,
11661159
return glyphQuads.length * 4;
11671160
}
11681161

1162+
export function getSizeDataFromKind(layerId: string, inputSizeData: SizeData, evaluatedTextSize: number, textScaleFactor: number,
1163+
minZoomSize: number, maxZoomSize: number
1164+
) {
1165+
const sizeData = inputSizeData;
1166+
let textSizeData: number[] = null;
1167+
1168+
if (sizeData.kind === 'source') {
1169+
textSizeData = [
1170+
SIZE_PACK_FACTOR * evaluatedTextSize * textScaleFactor
1171+
];
1172+
if (textSizeData[0] > MAX_PACKED_SIZE) {
1173+
warnOnce(`${layerId}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`);
1174+
}
1175+
} else if (sizeData.kind === 'composite') {
1176+
textSizeData = [
1177+
SIZE_PACK_FACTOR * minZoomSize * textScaleFactor,
1178+
SIZE_PACK_FACTOR * maxZoomSize * textScaleFactor
1179+
];
1180+
if (textSizeData[0] > MAX_PACKED_SIZE || textSizeData[1] > MAX_PACKED_SIZE) {
1181+
warnOnce(`${layerId}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`);
1182+
}
1183+
}
1184+
1185+
return textSizeData;
1186+
}
1187+
11691188
function getDefaultHorizontalShaping(horizontalShaping: Partial<Record<TextJustify, Shaping>>): Shaping | null {
11701189
// We don't care which shaping we get because this is used for collision purposes
11711190
// and all the justifications have the same collision box
@@ -1345,7 +1364,6 @@ function addSymbol(bucket: SymbolBucket,
13451364
// For more info check `updateVariableAnchors` in `draw_symbol.js` .
13461365

13471366
if (shapedIcon) {
1348-
const sizeData = bucket.iconSizeData;
13491367
const iconRotate = layer.layout.get('icon-rotate').evaluate(feature, {}, canonical);
13501368
const iconQuads = getIconQuads(shapedIcon, iconRotate, isSDFIcon, hasIconTextFit, sizes.iconScaleFactor);
13511369
const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate, isSDFIcon, hasIconTextFit, sizes.iconScaleFactor) : undefined;
@@ -1357,26 +1375,10 @@ function addSymbol(bucket: SymbolBucket,
13571375
hasIconTextFit);
13581376
numIconVertices = maxQuadCount * 4;
13591377

1360-
let iconSizeData = null;
1361-
1362-
if (sizeData.kind === 'source') {
1363-
iconSizeData = [
1364-
SIZE_PACK_FACTOR * layer.layout.get('icon-size').evaluate(feature, {}, canonical) * sizes.iconScaleFactor
1365-
];
1366-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1367-
if (iconSizeData[0] > MAX_PACKED_SIZE) {
1368-
warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`);
1369-
}
1370-
} else if (sizeData.kind === 'composite') {
1371-
iconSizeData = [
1372-
SIZE_PACK_FACTOR * sizes.compositeIconSizes[0].evaluate(feature, {}, canonical) * sizes.iconScaleFactor,
1373-
SIZE_PACK_FACTOR * sizes.compositeIconSizes[1].evaluate(feature, {}, canonical) * sizes.iconScaleFactor
1374-
];
1375-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1376-
if (iconSizeData[0] > MAX_PACKED_SIZE || iconSizeData[1] > MAX_PACKED_SIZE) {
1377-
warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`);
1378-
}
1379-
}
1378+
const evaluatedIconSize = layer.layout.get('icon-size').evaluate(feature, {}, canonical);
1379+
const minZoomSize = sizes.compositeIconSizes ? sizes.compositeIconSizes[0].evaluate(feature, {}, canonical) : 0;
1380+
const maxZoomSize = sizes.compositeIconSizes ? sizes.compositeIconSizes[1].evaluate(feature, {}, canonical) : 0;
1381+
const iconSizeData = getSizeDataFromKind(bucket.layerIds[0], bucket.iconSizeData, evaluatedIconSize, sizes.iconScaleFactor, minZoomSize, maxZoomSize);
13801382

13811383
bucket.addSymbols(
13821384
bucket.icon,
2 KB
Loading

test/integration/render-tests/appearance/brightness/style.json

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
"height": 64,
88
"width": 64,
99
"operations": [
10-
["wait"],
11-
["setPitch", 45],
1210
["wait"]
1311
]
1412
}
@@ -60,20 +58,29 @@
6058
}
6159
},
6260
"sprite": "local://sprites/sprite",
61+
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
6362
"layers": [
6463
{
6564
"id": "icon",
6665
"type": "symbol",
6766
"source": "geojson",
6867
"layout": {
69-
"icon-image": "bank-12"
68+
"icon-image": "bank-12",
69+
"text-field": "Test",
70+
"text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
71+
"text-size": 12,
72+
"text-offset": [0, 0],
73+
"text-rotate": 0
7074
},
7175
"appearances": [
7276
{
7377
"name": "test",
7478
"condition": [">=", ["measure-light", "brightness"], 0.5],
7579
"properties": {
76-
"icon-image": "bakery-12"
80+
"icon-image": "bakery-12",
81+
"text-size": 20,
82+
"text-offset": [1, 1],
83+
"text-rotate": 30
7784
}
7885
}
7986
]
9.84 KB
Loading

test/integration/render-tests/appearance/different-sizes/style.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,23 @@
2828
"layout": {
2929
"icon-image": "nine-part",
3030
"icon-allow-overlap": true,
31-
"icon-ignore-placement": true
31+
"icon-ignore-placement": true,
32+
"text-allow-overlap": true,
33+
"text-field": "Text",
34+
"text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
35+
"text-size": 14,
36+
"text-offset": [0, 0],
37+
"text-rotate": 0
3238
},
3339
"appearances": [
3440
{
3541
"name": "test",
3642
"condition": [">=", ["zoom"], 1],
3743
"properties": {
38-
"icon-image": "nine-part-content"
44+
"icon-image": "nine-part-content",
45+
"text-size": 22,
46+
"text-offset": [0.5, -0.5],
47+
"text-rotate": 20
3948
}
4049
}
4150
]

0 commit comments

Comments
 (0)