Skip to content

Commit

Permalink
move icon-text-fit to addFeature, mirroring native
Browse files Browse the repository at this point in the history
also add a lot more unit and render tests
  • Loading branch information
kkaefer committed Sep 16, 2019
1 parent 7d88452 commit 878e018
Show file tree
Hide file tree
Showing 33 changed files with 1,091 additions and 347 deletions.
14 changes: 6 additions & 8 deletions src/symbol/quads.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,19 @@ export type SymbolQuad = {
* Create the quads used for rendering an icon.
* @private
*/
export function getIconQuads(anchor: Anchor,
export function getIconQuads(
shapedIcon: PositionedIcon,
layer: SymbolStyleLayer,
alongLine: boolean,
shapedText: Shaping | null,
feature: Feature): Array<SymbolQuad> {
iconRotate: number): Array<SymbolQuad> {
const image = shapedIcon.image;
const layout = layer.layout;

// If you have a 10px icon that isn't perfectly aligned to the pixel grid it will cover 11 actual
// pixels. The quad needs to be padded to account for this, otherwise they'll look slightly clipped
// on one edge in some cases.
const border = 1;

// Expand the box to respect the 1 pixel border in the atlas image.
// Expand the box to respect the 1 pixel border in the atlas image. We're using `image.paddedRect - border`
// instead of image.displaySize because we only pad with one pixel for retina images as well, and the
// displaySize uses the logical dimensions, not the physical pixel dimensions.
const iconWidth = shapedIcon.right - shapedIcon.left;
const expandX = (iconWidth * image.paddedRect.w / (image.paddedRect.w - 2 * border) - iconWidth) / 2;
const left = shapedIcon.left - expandX;
Expand All @@ -74,7 +72,7 @@ export function getIconQuads(anchor: Anchor,
const br = new Point(right, bottom);
const bl = new Point(left, bottom);

const angle = layer.layout.get('icon-rotate').evaluate(feature, {}) * Math.PI / 180;
const angle = iconRotate * Math.PI / 180;

if (angle) {
const sin = Math.sin(angle),
Expand Down
102 changes: 47 additions & 55 deletions src/symbol/shaping.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow

import assert from 'assert';
import {
charHasUprightVerticalOrientation,
charAllowsIdeographicBreaking,
Expand All @@ -11,17 +12,15 @@ import ONE_EM from './one_em';

import type {StyleGlyph} from '../style/style_glyph';
import type {ImagePosition} from '../render/image_atlas';
import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
import Formatted from '../style-spec/expression/types/formatted';
import type {Feature} from '../style-spec/expression';

const WritingMode = {
horizontal: 1,
vertical: 2,
horizontalOnly: 3
};

export {shapeText, shapeIcon, getAnchorAlignment, WritingMode};
export {shapeText, shapeIcon, fitIconToText, getAnchorAlignment, WritingMode};

// The position of a glyph relative to the text's anchor point.
export type PositionedGlyph = {
Expand Down Expand Up @@ -571,63 +570,56 @@ export type PositionedIcon = {
right: number
};

function shapeIcon(image: ImagePosition,
layout: $PropertyType<SymbolStyleLayer, 'layout'>,
shapedText: Shaping | null,
feature: Feature) : PositionedIcon {
const textFit = layout.get('icon-text-fit');
const stretchWidth : boolean = textFit === 'width' || textFit === 'both';
const stretchHeight : boolean = textFit === 'height' || textFit === 'both';
function shapeIcon(image: ImagePosition, iconOffset: [number, number], iconAnchor: SymbolAnchor): PositionedIcon {
const {horizontalAlign, verticalAlign} = getAnchorAlignment(iconAnchor);
const dx = iconOffset[0];
const dy = iconOffset[1];
const x1 = dx - image.displaySize[0] * horizontalAlign;
const x2 = x1 + image.displaySize[0];
const y1 = dy - image.displaySize[1] * verticalAlign;
const y2 = y1 + image.displaySize[1];
return {image, top: y1, bottom: y2, left: x1, right: x2};
}

let top, right, bottom, left;
if (shapedText && (stretchWidth || stretchHeight)) {
// We don't respect the icon-anchor, because icon-text-fit is set. Instead, the icon will be
// centered on the text, then stretched in the given dimensions.
const size = layout.get('text-size').evaluate(feature, {}) / 24;
const padding = layout.get('icon-text-fit-padding');

const textLeft = shapedText.left * size;
const textRight = shapedText.right * size;
if (stretchWidth) {
// Stretched horizontally to the text width
left = textLeft - padding[3];
right = textRight + padding[1];
} else {
// Centered on the text
left = (textLeft + textRight - image.displaySize[0]) / 2;
right = left + image.displaySize[0];
}
function fitIconToText(shapedIcon: PositionedIcon, shapedText: Shaping,
textFit: string,
padding: [ number, number, number, number ],
iconOffset: [ number, number ], fontScale: number): PositionedIcon {
assert(textFit !== 'none');
assert(Array.isArray(padding) && padding.length === 4);
assert(Array.isArray(iconOffset) && iconOffset.length === 2);

const textTop = shapedText.top * size;
const textBottom = shapedText.bottom * size;
if (stretchHeight) {
// Stretched vertically to the text height
top = textTop - padding[0];
bottom = textBottom + padding[2];
} else {
// Centered on the text
top = (textTop + textBottom - image.displaySize[1]) / 2;
bottom = top + image.displaySize[1];
}
} else {
// No icon-text-fit. We're going to respect the icon-anchor alignment.
const iconAnchor = layout.get('icon-anchor').evaluate(feature, {});
const { horizontalAlign, verticalAlign } = getAnchorAlignment(iconAnchor);
const image = shapedIcon.image;

// We don't respect the icon-anchor, because icon-text-fit is set. Instead,
// the icon will be centered on the text, then stretched in the given
// dimensions.

const textLeft = shapedText.left * fontScale;
const textRight = shapedText.right * fontScale;

left = -image.displaySize[0] * horizontalAlign;
let top, right, bottom, left;
if (textFit === 'width' || textFit === 'both') {
// Stretched horizontally to the text width
left = iconOffset[0] + textLeft - padding[3];
right = iconOffset[0] + textRight + padding[1];
} else {
// Centered on the text
left = iconOffset[0] + (textLeft + textRight - image.displaySize[0]) / 2;
right = left + image.displaySize[0];
top = -image.displaySize[1] * verticalAlign;
bottom = top + image.displaySize[1];
}

// Finally, apply the icon offset in all cases.
const [offsetX, offsetY] = layout.get('icon-offset').evaluate(feature, {});
const textTop = shapedText.top * fontScale;
const textBottom = shapedText.bottom * fontScale;
if (textFit === 'height' || textFit === 'both') {
// Stretched vertically to the text height
top = iconOffset[1] + textTop - padding[0];
bottom = iconOffset[1] + textBottom + padding[2];
} else {
// Centered on the text
top = iconOffset[1] + (textTop + textBottom - image.displaySize[1]) / 2;
bottom = top + image.displaySize[1];
}

return {
image,
top: top + offsetY,
right: right + offsetX,
bottom: bottom + offsetY,
left: left + offsetX
};
return {image, top, right, bottom, left};
}
20 changes: 13 additions & 7 deletions src/symbol/symbol_layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Anchor from './anchor';

import {getAnchors, getCenterAnchor} from './get_anchors';
import clipLine from './clip_line';
import {shapeText, shapeIcon, WritingMode} from './shaping';
import {shapeText, shapeIcon, WritingMode, fitIconToText} from './shaping';
import {getGlyphQuads, getIconQuads} from './quads';
import CollisionFeature from './collision_feature';
import {warnOnce} from '../util/util';
Expand Down Expand Up @@ -293,9 +293,8 @@ export function performSymbolLayout(bucket: SymbolBucket,
if (image) {
shapedIcon = shapeIcon(
imagePositions[feature.icon],
layout,
getDefaultHorizontalShaping(shapedTextOrientations.horizontal),
feature);
layout.get('icon-offset').evaluate(feature, {}),
layout.get('icon-anchor').evaluate(feature, {}));
if (bucket.sdfIcons === undefined) {
bucket.sdfIcons = image.sdf;
} else if (bucket.sdfIcons !== image.sdf) {
Expand Down Expand Up @@ -377,6 +376,15 @@ function addFeature(bucket: SymbolBucket,
symbolPlacement = layout.get('symbol-placement'),
textRepeatDistance = symbolMinDistance / 2;

const iconTextFit = layout.get('icon-text-fit');
// Adjust shaped icon size when icon-text-fit is used.
if (shapedIcon && iconTextFit !== 'none') {
if (defaultHorizontalShaping) {
shapedIcon = fitIconToText(shapedIcon, defaultHorizontalShaping, iconTextFit,
layout.get('icon-text-fit-padding'), iconOffset, fontScale);
}
}

const addSymbolAtAnchor = (line, anchor) => {
if (anchor.x < 0 || anchor.x >= EXTENT || anchor.y < 0 || anchor.y >= EXTENT) {
// Symbol layers are drawn across tile boundaries, We filter out symbols
Expand Down Expand Up @@ -605,10 +613,8 @@ function addSymbol(bucket: SymbolBucket,
const verticalTextBoxEndIndex = verticalTextCollisionFeature ? verticalTextCollisionFeature.boxEndIndex : bucket.collisionBoxArray.length;

if (shapedIcon) {
const iconQuads = getIconQuads(anchor, shapedIcon, layer,
iconAlongLine, getDefaultHorizontalShaping(shapedTextOrientations.horizontal),
feature);
const iconRotate = layer.layout.get('icon-rotate').evaluate(feature, {});
const iconQuads = getIconQuads(shapedIcon, iconRotate);
iconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, /*align boxes to line*/false, bucket.overscaling, iconRotate);

numIconVertices = iconQuads.length * 4;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 878e018

Please sign in to comment.