diff --git a/bench/benchmarks/layers.js b/bench/benchmarks/layers.js index e56c5f0d2fb..a251a07a40a 100644 --- a/bench/benchmarks/layers.js +++ b/bench/benchmarks/layers.js @@ -220,3 +220,22 @@ export class LayerSymbol extends LayerBenchmark { }); } } + +export class LayerSymbolWithIcons extends LayerBenchmark { + constructor() { + super(); + + this.layerStyle = Object.assign({}, style, { + layers: generateLayers({ + 'id': 'symbollayer', + 'type': 'symbol', + 'source': 'composite', + 'source-layer': 'poi_label', + 'layout': { + 'icon-image': 'dot-11', + 'text-field': ['format', ['get', 'name_en'], ['image', 'dot-11']] + } + }) + }); + } +} diff --git a/bench/versions/benchmarks.js b/bench/versions/benchmarks.js index 44eba7b9d5b..6cab2a05299 100644 --- a/bench/versions/benchmarks.js +++ b/bench/versions/benchmarks.js @@ -9,7 +9,7 @@ import WorkerTransfer from '../benchmarks/worker_transfer'; import Paint from '../benchmarks/paint'; import PaintStates from '../benchmarks/paint_states'; import {PropertyLevelRemove, FeatureLevelRemove, SourceLevelRemove} from '../benchmarks/remove_paint_state'; -import {LayerBackground, LayerCircle, LayerFill, LayerFillExtrusion, LayerHeatmap, LayerHillshade, LayerLine, LayerRaster, LayerSymbol} from '../benchmarks/layers'; +import {LayerBackground, LayerCircle, LayerFill, LayerFillExtrusion, LayerHeatmap, LayerHillshade, LayerLine, LayerRaster, LayerSymbol, LayerSymbolWithIcons} from '../benchmarks/layers'; import Load from '../benchmarks/map_load'; import Validate from '../benchmarks/style_validate'; import StyleLayerCreate from '../benchmarks/style_layer_create'; @@ -63,6 +63,7 @@ register('LayerHillshade', new LayerHillshade()); register('LayerLine', new LayerLine()); register('LayerRaster', new LayerRaster()); register('LayerSymbol', new LayerSymbol()); +register('LayerSymbolWithIcons', new LayerSymbolWithIcons()); register('Load', new Load()); register('LayoutDDS', new LayoutDDS()); register('SymbolLayout', new SymbolLayout(style, styleLocations.map(location => location.tileID[0]))); diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 4746935a53c..925e8e70499 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -33,6 +33,7 @@ const vectorTileFeatureTypes = mvt.VectorTileFeature.types; import {verticalizedCharacterMap} from '../../util/verticalize_punctuation'; import Anchor from '../../symbol/anchor'; import {getSizeData} from '../../symbol/symbol_size'; +import {MAX_PACKED_SIZE} from '../../symbol/symbol_layout'; import {register} from '../../util/web_worker_transfer'; import EvaluationParameters from '../../style/evaluation_parameters'; import Formatted from '../../style-spec/expression/types/formatted'; @@ -101,7 +102,9 @@ const shaderOpacityAttributes = [ {name: 'a_fade_opacity', components: 1, type: 'Uint8', offset: 0} ]; -function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex) { +function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex, isSDF: boolean) { + const aSizeX = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[0])) : 0; + const aSizeY = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[1])) : 0; array.emplaceBack( // a_pos_offset anchorX, @@ -112,8 +115,8 @@ function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex) { // a_data tx, // x coordinate of symbol on glyph atlas texture ty, // y coordinate of symbol on glyph atlas texture - sizeVertex ? sizeVertex[0] : 0, - sizeVertex ? sizeVertex[1] : 0 + (aSizeX << 1) + (isSDF ? 1 : 0), + aSizeY ); } @@ -271,6 +274,7 @@ class SymbolBucket implements Bucket { index: number; sdfIcons: boolean; + iconsInText: boolean; iconsNeedLinear: boolean; bucketInstanceId: number; justReloaded: boolean; @@ -381,7 +385,9 @@ class SymbolBucket implements Bucket { const textField = layout.get('text-field'); const iconImage = layout.get('icon-image'); const hasText = - (textField.value.kind !== 'constant' || textField.value.value.toString().length > 0) && + (textField.value.kind !== 'constant' || + (textField.value.value instanceof Formatted && !textField.value.value.isEmpty()) || + textField.value.value.toString().length > 0) && (textFont.value.kind !== 'constant' || textFont.value.value.length > 0); // we should always resolve the icon-image value if the property was defined in the style // this allows us to fire the styleimagemissing event if image evaluation returns null @@ -470,10 +476,15 @@ class SymbolBucket implements Bucket { const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point'; this.allowVerticalPlacement = this.writingModes && this.writingModes.indexOf(WritingMode.vertical) >= 0; for (const section of text.sections) { - const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString()); - const sectionFont = section.fontStack || fontStack; - const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {}; - this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, this.allowVerticalPlacement, doesAllowVerticalWritingMode); + if (!section.image) { + const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString()); + const sectionFont = section.fontStack || fontStack; + const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {}; + this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, this.allowVerticalPlacement, doesAllowVerticalWritingMode); + } else { + // Add section image to the list of dependencies. + icons[section.image.name] = true; + } } } } @@ -587,10 +598,10 @@ class SymbolBucket implements Bucket { const index = segment.vertexLength; const y = symbol.glyphOffset[1]; - addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex); - addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex); - addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex); - addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex, symbol.isSDF); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex, symbol.isSDF); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex, symbol.isSDF); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, symbol.isSDF); addDynamicAttributes(dynamicLayoutVertexArray, labelAnchor, angle); diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index ff8c06cdaee..fb452721628 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -20,7 +20,8 @@ import {evaluateVariableOffset} from '../symbol/symbol_layout'; import { symbolIconUniformValues, - symbolSDFUniformValues + symbolSDFUniformValues, + symbolTextAndIconUniformValues } from './program/symbol_program'; import type Painter from './painter'; @@ -43,7 +44,9 @@ type SymbolTileRenderState = { buffers: SymbolBuffers, uniformValues: any, atlasTexture: Texture, + atlasTextureIcon: Texture | null, atlasInterpolation: any, + atlasInterpolationIcon: any, isSDF: boolean, hasHalo: boolean } @@ -208,6 +211,16 @@ function updateVariableAnchorsForBucket(bucket, rotateWithMap, pitchWithMap, var bucket.text.dynamicLayoutVertexBuffer.updateData(dynamicTextLayoutVertexArray); } +function getSymbolProgramName(isSDF: boolean, isText: boolean, bucket: SymbolBucket) { + if (bucket.iconsInText && isText) { + return 'symbolTextAndIcon'; + } else if (isSDF) { + return 'symbolSDF'; + } else { + return 'symbolIcon'; + } +} + function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate, translateAnchor, rotationAlignment, pitchAlignment, keepUpright, stencilMode, colorMode) { @@ -244,28 +257,33 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const isSDF = isText || bucket.sdfIcons; const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; + const transformed = pitchWithMap || tr.pitch !== 0; if (!program) { - program = painter.useProgram(isSDF ? 'symbolSDF' : 'symbolIcon', programConfiguration); + program = painter.useProgram(getSymbolProgramName(isSDF, isText, bucket), programConfiguration); size = symbolSize.evaluateSizeForZoom(sizeData, tr.zoom); } - context.activeTexture.set(gl.TEXTURE0); - let texSize: [number, number]; + let texSizeIcon: [number, number] = [0, 0]; let atlasTexture; let atlasInterpolation; + let atlasTextureIcon = null; + let atlasInterpolationIcon; if (isText) { atlasTexture = tile.glyphAtlasTexture; atlasInterpolation = gl.LINEAR; texSize = tile.glyphAtlasTexture.size; - + if (bucket.iconsInText) { + texSizeIcon = tile.imageAtlasTexture.size; + atlasTextureIcon = tile.imageAtlasTexture; + const zoomDependentSize = sizeData.kind === 'composite' || sizeData.kind === 'camera'; + atlasInterpolationIcon = transformed || painter.options.rotating || painter.options.zooming || zoomDependentSize ? gl.LINEAR : gl.NEAREST; + } } else { const iconScaled = layer.layout.get('icon-size').constantOr(0) !== 1 || bucket.iconsNeedLinear; - const iconTransformed = pitchWithMap || tr.pitch !== 0; - atlasTexture = tile.imageAtlasTexture; - atlasInterpolation = isSDF || painter.options.rotating || painter.options.zooming || iconScaled || iconTransformed ? + atlasInterpolation = isSDF || painter.options.rotating || painter.options.zooming || iconScaled || transformed ? gl.LINEAR : gl.NEAREST; texSize = tile.imageAtlasTexture.size; @@ -292,10 +310,15 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate let uniformValues; if (isSDF) { - uniformValues = symbolSDFUniformValues(sizeData.kind, + if (!bucket.iconsInText) { + uniformValues = symbolSDFUniformValues(sizeData.kind, size, rotateInShader, pitchWithMap, painter, matrix, uLabelPlaneMatrix, uglCoordMatrix, isText, texSize, true); - + } else { + uniformValues = symbolTextAndIconUniformValues(sizeData.kind, + size, rotateInShader, pitchWithMap, painter, matrix, + uLabelPlaneMatrix, uglCoordMatrix, texSize, texSizeIcon); + } } else { uniformValues = symbolIconUniformValues(sizeData.kind, size, rotateInShader, pitchWithMap, painter, matrix, @@ -307,7 +330,9 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate buffers, uniformValues, atlasTexture, + atlasTextureIcon, atlasInterpolation, + atlasInterpolationIcon, isSDF, hasHalo }; @@ -337,7 +362,14 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate for (const segmentState of tileRenderState) { const state = segmentState.state; + context.activeTexture.set(gl.TEXTURE0); state.atlasTexture.bind(state.atlasInterpolation, gl.CLAMP_TO_EDGE); + if (state.atlasTextureIcon) { + context.activeTexture.set(gl.TEXTURE1); + if (state.atlasTextureIcon) { + state.atlasTextureIcon.bind(state.atlasInterpolationIcon, gl.CLAMP_TO_EDGE); + } + } if (state.isSDF) { const uniformValues = ((state.uniformValues: any): UniformValues); diff --git a/src/render/glyph_atlas.js b/src/render/glyph_atlas.js index 584b8816fbd..a6bcd71a0db 100644 --- a/src/render/glyph_atlas.js +++ b/src/render/glyph_atlas.js @@ -8,7 +8,7 @@ import type {GlyphMetrics, StyleGlyph} from '../style/style_glyph'; const padding = 1; -type Rect = { +export type Rect = { x: number, y: number, w: number, diff --git a/src/render/image_atlas.js b/src/render/image_atlas.js index 270be1191ee..ebf78823be9 100644 --- a/src/render/image_atlas.js +++ b/src/render/image_atlas.js @@ -8,7 +8,8 @@ import type {StyleImage} from '../style/style_image'; import type ImageManager from './image_manager'; import type Texture from './texture'; -const padding = 1; +const IMAGE_PADDING = 1; +export {IMAGE_PADDING}; type Rect = { x: number, @@ -30,15 +31,15 @@ export class ImagePosition { get tl(): [number, number] { return [ - this.paddedRect.x + padding, - this.paddedRect.y + padding + this.paddedRect.x + IMAGE_PADDING, + this.paddedRect.y + IMAGE_PADDING ]; } get br(): [number, number] { return [ - this.paddedRect.x + this.paddedRect.w - padding, - this.paddedRect.y + this.paddedRect.h - padding + this.paddedRect.x + this.paddedRect.w - IMAGE_PADDING, + this.paddedRect.y + this.paddedRect.h - IMAGE_PADDING ]; } @@ -48,8 +49,8 @@ export class ImagePosition { get displaySize(): [number, number] { return [ - (this.paddedRect.w - padding * 2) / this.pixelRatio, - (this.paddedRect.h - padding * 2) / this.pixelRatio + (this.paddedRect.w - IMAGE_PADDING * 2) / this.pixelRatio, + (this.paddedRect.h - IMAGE_PADDING * 2) / this.pixelRatio ]; } } @@ -76,14 +77,14 @@ export default class ImageAtlas { for (const id in icons) { const src = icons[id]; const bin = iconPositions[id].paddedRect; - RGBAImage.copy(src.data, image, {x: 0, y: 0}, {x: bin.x + padding, y: bin.y + padding}, src.data); + RGBAImage.copy(src.data, image, {x: 0, y: 0}, {x: bin.x + IMAGE_PADDING, y: bin.y + IMAGE_PADDING}, src.data); } for (const id in patterns) { const src = patterns[id]; const bin = patternPositions[id].paddedRect; - const x = bin.x + padding, - y = bin.y + padding, + const x = bin.x + IMAGE_PADDING, + y = bin.y + IMAGE_PADDING, w = src.data.width, h = src.data.height; @@ -106,8 +107,8 @@ export default class ImageAtlas { const bin = { x: 0, y: 0, - w: src.data.width + 2 * padding, - h: src.data.height + 2 * padding, + w: src.data.width + 2 * IMAGE_PADDING, + h: src.data.height + 2 * IMAGE_PADDING, }; bins.push(bin); positions[id] = new ImagePosition(bin, src); diff --git a/src/render/program/program_uniforms.js b/src/render/program/program_uniforms.js index b44bd7d802b..9ccc73bfd64 100644 --- a/src/render/program/program_uniforms.js +++ b/src/render/program/program_uniforms.js @@ -10,7 +10,7 @@ import {heatmapUniforms, heatmapTextureUniforms} from './heatmap_program'; import {hillshadeUniforms, hillshadePrepareUniforms} from './hillshade_program'; import {lineUniforms, lineGradientUniforms, linePatternUniforms, lineSDFUniforms} from './line_program'; import {rasterUniforms} from './raster_program'; -import {symbolIconUniforms, symbolSDFUniforms} from './symbol_program'; +import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from './symbol_program'; import {backgroundUniforms, backgroundPatternUniforms} from './background_program'; export const programUniforms = { @@ -36,6 +36,7 @@ export const programUniforms = { raster: rasterUniforms, symbolIcon: symbolIconUniforms, symbolSDF: symbolSDFUniforms, + symbolTextAndIcon: symbolTextAndIconUniforms, background: backgroundUniforms, backgroundPattern: backgroundPatternUniforms }; diff --git a/src/render/program/symbol_program.js b/src/render/program/symbol_program.js index 273a261d706..98d6d5bb93f 100644 --- a/src/render/program/symbol_program.js +++ b/src/render/program/symbol_program.js @@ -54,6 +54,30 @@ export type SymbolSDFUniformsType = {| 'u_is_halo': Uniform1f |}; +export type symbolTextAndIconUniformsType = {| + 'u_is_size_zoom_constant': Uniform1i, + 'u_is_size_feature_constant': Uniform1i, + 'u_size_t': Uniform1f, + 'u_size': Uniform1f, + 'u_camera_to_center_distance': Uniform1f, + 'u_pitch': Uniform1f, + 'u_rotate_symbol': Uniform1i, + 'u_aspect_ratio': Uniform1f, + 'u_fade_change': Uniform1f, + 'u_matrix': UniformMatrix4f, + 'u_label_plane_matrix': UniformMatrix4f, + 'u_coord_matrix': UniformMatrix4f, + 'u_is_text': Uniform1f, + 'u_pitch_with_map': Uniform1i, + 'u_texsize': Uniform2f, + 'u_texsize_icon': Uniform2f, + 'u_texture': Uniform1i, + 'u_texture_icon': Uniform1i, + 'u_gamma_scale': Uniform1f, + 'u_device_pixel_ratio': Uniform1f, + 'u_is_halo': Uniform1f +|}; + const symbolIconUniforms = (context: Context, locations: UniformLocations): SymbolIconUniformsType => ({ 'u_is_size_zoom_constant': new Uniform1i(context, locations.u_is_size_zoom_constant), 'u_is_size_feature_constant': new Uniform1i(context, locations.u_is_size_feature_constant), @@ -95,6 +119,30 @@ const symbolSDFUniforms = (context: Context, locations: UniformLocations): Symbo 'u_is_halo': new Uniform1f(context, locations.u_is_halo) }); +const symbolTextAndIconUniforms = (context: Context, locations: UniformLocations): symbolTextAndIconUniformsType => ({ + 'u_is_size_zoom_constant': new Uniform1i(context, locations.u_is_size_zoom_constant), + 'u_is_size_feature_constant': new Uniform1i(context, locations.u_is_size_feature_constant), + 'u_size_t': new Uniform1f(context, locations.u_size_t), + 'u_size': new Uniform1f(context, locations.u_size), + 'u_camera_to_center_distance': new Uniform1f(context, locations.u_camera_to_center_distance), + 'u_pitch': new Uniform1f(context, locations.u_pitch), + 'u_rotate_symbol': new Uniform1i(context, locations.u_rotate_symbol), + 'u_aspect_ratio': new Uniform1f(context, locations.u_aspect_ratio), + 'u_fade_change': new Uniform1f(context, locations.u_fade_change), + 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), + 'u_label_plane_matrix': new UniformMatrix4f(context, locations.u_label_plane_matrix), + 'u_coord_matrix': new UniformMatrix4f(context, locations.u_coord_matrix), + 'u_is_text': new Uniform1f(context, locations.u_is_text), + 'u_pitch_with_map': new Uniform1i(context, locations.u_pitch_with_map), + 'u_texsize': new Uniform2f(context, locations.u_texsize), + 'u_texsize_icon': new Uniform2f(context, locations.u_texsize_icon), + 'u_texture': new Uniform1i(context, locations.u_texture), + 'u_texture_icon': new Uniform1i(context, locations.u_texture_icon), + 'u_gamma_scale': new Uniform1f(context, locations.u_gamma_scale), + 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), + 'u_is_halo': new Uniform1f(context, locations.u_is_halo) +}); + const symbolIconUniformValues = ( functionType: string, size: ?{uSizeT: number, uSize: number}, @@ -153,4 +201,24 @@ const symbolSDFUniformValues = ( }); }; -export {symbolIconUniforms, symbolSDFUniforms, symbolIconUniformValues, symbolSDFUniformValues}; +const symbolTextAndIconUniformValues = ( + functionType: string, + size: ?{uSizeT: number, uSize: number}, + rotateInShader: boolean, + pitchWithMap: boolean, + painter: Painter, + matrix: Float32Array, + labelPlaneMatrix: Float32Array, + glCoordMatrix: Float32Array, + texSizeSDF: [number, number], + texSizeIcon: [number, number] +): UniformValues => { + return extend(symbolSDFUniformValues(functionType, size, + rotateInShader, pitchWithMap, painter, matrix, labelPlaneMatrix, + glCoordMatrix, true, texSizeSDF, true), { + 'u_texsize_icon': texSizeIcon, + 'u_texture_icon': 1 + }); +}; + +export {symbolIconUniforms, symbolSDFUniforms, symbolIconUniformValues, symbolSDFUniformValues, symbolTextAndIconUniformValues, symbolTextAndIconUniforms}; diff --git a/src/shaders/shaders.js b/src/shaders/shaders.js index 13b20267df1..221728dfddd 100644 --- a/src/shaders/shaders.js +++ b/src/shaders/shaders.js @@ -52,6 +52,8 @@ import symbolIconFrag from './symbol_icon.fragment.glsl'; import symbolIconVert from './symbol_icon.vertex.glsl'; import symbolSDFFrag from './symbol_sdf.fragment.glsl'; import symbolSDFVert from './symbol_sdf.vertex.glsl'; +import symbolTextAndIconFrag from './symbol_text_and_icon.fragment.glsl'; +import symbolTextAndIconVert from './symbol_text_and_icon.vertex.glsl'; export const prelude = compile(preludeFrag, preludeVert); export const background = compile(backgroundFrag, backgroundVert); @@ -78,6 +80,7 @@ export const lineSDF = compile(lineSDFFrag, lineSDFVert); export const raster = compile(rasterFrag, rasterVert); export const symbolIcon = compile(symbolIconFrag, symbolIconVert); export const symbolSDF = compile(symbolSDFFrag, symbolSDFVert); +export const symbolTextAndIcon = compile(symbolTextAndIconFrag, symbolTextAndIconVert); // Expand #pragmas to #ifdefs. diff --git a/src/shaders/symbol_icon.vertex.glsl b/src/shaders/symbol_icon.vertex.glsl index d0b24e73d9f..056ce24e4d2 100644 --- a/src/shaders/symbol_icon.vertex.glsl +++ b/src/shaders/symbol_icon.vertex.glsl @@ -38,15 +38,14 @@ void main() { vec2 a_tex = a_data.xy; vec2 a_size = a_data.zw; + float a_size_min = floor(a_size[0] * 0.5); highp float segment_angle = -a_projected_pos[2]; - float size; + if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { - size = mix(a_size[0], a_size[1], u_size_t) / 256.0; + size = mix(a_size_min, a_size[1], u_size_t) / 128.0; } else if (u_is_size_zoom_constant && !u_is_size_feature_constant) { - size = a_size[0] / 256.0; - } else if (!u_is_size_zoom_constant && u_is_size_feature_constant) { - size = u_size; + size = a_size_min / 128.0; } else { size = u_size; } diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl index 5f80016b87f..6c9ef11f833 100644 --- a/src/shaders/symbol_sdf.vertex.glsl +++ b/src/shaders/symbol_sdf.vertex.glsl @@ -50,15 +50,14 @@ void main() { vec2 a_tex = a_data.xy; vec2 a_size = a_data.zw; + float a_size_min = floor(a_size[0] * 0.5); highp float segment_angle = -a_projected_pos[2]; float size; if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { - size = mix(a_size[0], a_size[1], u_size_t) / 256.0; + size = mix(a_size_min, a_size[1], u_size_t) / 128.0; } else if (u_is_size_zoom_constant && !u_is_size_feature_constant) { - size = a_size[0] / 256.0; - } else if (!u_is_size_zoom_constant && u_is_size_feature_constant) { - size = u_size; + size = a_size_min / 128.0; } else { size = u_size; } @@ -104,11 +103,10 @@ void main() { gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale), 0.0, 1.0); float gamma_scale = gl_Position.w; - vec2 tex = a_tex / u_texsize; vec2 fade_opacity = unpack_opacity(a_fade_opacity); float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change)); - v_data0 = vec2(tex.x, tex.y); + v_data0 = a_tex / u_texsize; v_data1 = vec3(gamma_scale, size, interpolated_fade_opacity); } diff --git a/src/shaders/symbol_text_and_icon.fragment.glsl b/src/shaders/symbol_text_and_icon.fragment.glsl new file mode 100644 index 00000000000..6220563c415 --- /dev/null +++ b/src/shaders/symbol_text_and_icon.fragment.glsl @@ -0,0 +1,68 @@ +#define SDF_PX 8.0 + +#define SDF 1.0 +#define ICON 0.0 + +uniform bool u_is_halo; +uniform sampler2D u_texture; +uniform sampler2D u_texture_icon; +uniform highp float u_gamma_scale; +uniform lowp float u_device_pixel_ratio; + +varying vec4 v_data0; +varying vec4 v_data1; + +#pragma mapbox: define highp vec4 fill_color +#pragma mapbox: define highp vec4 halo_color +#pragma mapbox: define lowp float opacity +#pragma mapbox: define lowp float halo_width +#pragma mapbox: define lowp float halo_blur + +void main() { + #pragma mapbox: initialize highp vec4 fill_color + #pragma mapbox: initialize highp vec4 halo_color + #pragma mapbox: initialize lowp float opacity + #pragma mapbox: initialize lowp float halo_width + #pragma mapbox: initialize lowp float halo_blur + + float fade_opacity = v_data1[2]; + + if (v_data1.w == ICON) { + vec2 tex_icon = v_data0.zw; + lowp float alpha = opacity * fade_opacity; + gl_FragColor = texture2D(u_texture_icon, tex_icon) * alpha; + +#ifdef OVERDRAW_INSPECTOR + gl_FragColor = vec4(1.0); +#endif + return; + } + + vec2 tex = v_data0.xy; + + float EDGE_GAMMA = 0.105 / u_device_pixel_ratio; + + float gamma_scale = v_data1.x; + float size = v_data1.y; + + float fontScale = size / 24.0; + + lowp vec4 color = fill_color; + highp float gamma = EDGE_GAMMA / (fontScale * u_gamma_scale); + lowp float buff = (256.0 - 64.0) / 256.0; + if (u_is_halo) { + color = halo_color; + gamma = (halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale); + buff = (6.0 - halo_width / fontScale) / SDF_PX; + } + + lowp float dist = texture2D(u_texture, tex).a; + highp float gamma_scaled = gamma * gamma_scale; + highp float alpha = smoothstep(buff - gamma_scaled, buff + gamma_scaled, dist); + + gl_FragColor = color * (alpha * opacity * fade_opacity); + +#ifdef OVERDRAW_INSPECTOR + gl_FragColor = vec4(1.0); +#endif +} diff --git a/src/shaders/symbol_text_and_icon.vertex.glsl b/src/shaders/symbol_text_and_icon.vertex.glsl new file mode 100644 index 00000000000..647310fc9c9 --- /dev/null +++ b/src/shaders/symbol_text_and_icon.vertex.glsl @@ -0,0 +1,116 @@ +const float PI = 3.141592653589793; + +attribute vec4 a_pos_offset; +attribute vec4 a_data; +attribute vec3 a_projected_pos; +attribute float a_fade_opacity; + +// contents of a_size vary based on the type of property value +// used for {text,icon}-size. +// For constants, a_size is disabled. +// For source functions, we bind only one value per vertex: the value of {text,icon}-size evaluated for the current feature. +// For composite functions: +// [ text-size(lowerZoomStop, feature), +// text-size(upperZoomStop, feature) ] +uniform bool u_is_size_zoom_constant; +uniform bool u_is_size_feature_constant; +uniform highp float u_size_t; // used to interpolate between zoom stops when size is a composite function +uniform highp float u_size; // used when size is both zoom and feature constant +uniform mat4 u_matrix; +uniform mat4 u_label_plane_matrix; +uniform mat4 u_coord_matrix; +uniform bool u_is_text; +uniform bool u_pitch_with_map; +uniform highp float u_pitch; +uniform bool u_rotate_symbol; +uniform highp float u_aspect_ratio; +uniform highp float u_camera_to_center_distance; +uniform float u_fade_change; +uniform vec2 u_texsize; +uniform vec2 u_texsize_icon; + +varying vec4 v_data0; +varying vec4 v_data1; + +#pragma mapbox: define highp vec4 fill_color +#pragma mapbox: define highp vec4 halo_color +#pragma mapbox: define lowp float opacity +#pragma mapbox: define lowp float halo_width +#pragma mapbox: define lowp float halo_blur + +void main() { + #pragma mapbox: initialize highp vec4 fill_color + #pragma mapbox: initialize highp vec4 halo_color + #pragma mapbox: initialize lowp float opacity + #pragma mapbox: initialize lowp float halo_width + #pragma mapbox: initialize lowp float halo_blur + + vec2 a_pos = a_pos_offset.xy; + vec2 a_offset = a_pos_offset.zw; + + vec2 a_tex = a_data.xy; + vec2 a_size = a_data.zw; + + float a_size_min = floor(a_size[0] * 0.5); + float is_sdf = a_size[0] - 2.0 * a_size_min; + + highp float segment_angle = -a_projected_pos[2]; + float size; + + if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { + size = mix(a_size_min, a_size[1], u_size_t) / 128.0; + } else if (u_is_size_zoom_constant && !u_is_size_feature_constant) { + size = a_size_min / 128.0; + } else { + size = u_size; + } + + vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1); + highp float camera_to_anchor_distance = projectedPoint.w; + // If the label is pitched with the map, layout is done in pitched space, + // which makes labels in the distance smaller relative to viewport space. + // We counteract part of that effect by multiplying by the perspective ratio. + // If the label isn't pitched with the map, we do layout in viewport space, + // which makes labels in the distance larger relative to the features around + // them. We counteract part of that effect by dividing by the perspective ratio. + highp float distance_ratio = u_pitch_with_map ? + camera_to_anchor_distance / u_camera_to_center_distance : + u_camera_to_center_distance / camera_to_anchor_distance; + highp float perspective_ratio = clamp( + 0.5 + 0.5 * distance_ratio, + 0.0, // Prevents oversized near-field symbols in pitched/overzoomed tiles + 4.0); + + size *= perspective_ratio; + + float fontScale = size / 24.0; + + highp float symbol_rotation = 0.0; + if (u_rotate_symbol) { + // Point labels with 'rotation-alignment: map' are horizontal with respect to tile units + // To figure out that angle in projected space, we draw a short horizontal line in tile + // space, project it, and measure its angle in projected space. + vec4 offsetProjectedPoint = u_matrix * vec4(a_pos + vec2(1, 0), 0, 1); + + vec2 a = projectedPoint.xy / projectedPoint.w; + vec2 b = offsetProjectedPoint.xy / offsetProjectedPoint.w; + + symbol_rotation = atan((b.y - a.y) / u_aspect_ratio, b.x - a.x); + } + + highp float angle_sin = sin(segment_angle + symbol_rotation); + highp float angle_cos = cos(segment_angle + symbol_rotation); + mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos); + + vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0); + gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale), 0.0, 1.0); + float gamma_scale = gl_Position.w; + + vec2 fade_opacity = unpack_opacity(a_fade_opacity); + float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; + float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change)); + + v_data0.xy = a_tex / u_texsize; + v_data0.zw = a_tex / u_texsize_icon; + v_data1 = vec4(gamma_scale, size, interpolated_fade_opacity, is_sdf); +} diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index 9f30b0722b0..8f82185794f 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -118,7 +118,7 @@ class Coercion implements Expression { serialize() { if (this.type.kind === 'formatted') { - return new FormatExpression([{text: this.args[0], scale: null, font: null, textColor: null}]).serialize(); + return new FormatExpression([{content: this.args[0], scale: null, font: null, textColor: null}]).serialize(); } if (this.type.kind === 'resolvedImage') { diff --git a/src/style-spec/expression/definitions/format.js b/src/style-spec/expression/definitions/format.js index 6d9694cbe21..002cc8e41ab 100644 --- a/src/style-spec/expression/definitions/format.js +++ b/src/style-spec/expression/definitions/format.js @@ -1,8 +1,8 @@ // @flow -import {NumberType, ValueType, FormattedType, array, StringType, ColorType} from '../types'; +import {NumberType, ValueType, FormattedType, array, StringType, ColorType, ResolvedImageType} from '../types'; import Formatted, {FormattedSection} from '../types/formatted'; -import {toString} from '../values'; +import {toString, typeOf} from '../values'; import type {Expression} from '../expression'; import type EvaluationContext from '../evaluation_context'; @@ -10,7 +10,9 @@ import type ParsingContext from '../parsing_context'; import type {Type} from '../types'; type FormattedSectionExpression = { - text: Expression, + // Content of a section may be Image expression or other + // type of expression that is coercable to 'string'. + content: Expression, scale: Expression | null; font: Expression | null; textColor: Expression | null; @@ -26,65 +28,83 @@ export default class FormatExpression implements Expression { } static parse(args: $ReadOnlyArray, context: ParsingContext): ?Expression { - if (args.length < 3) { - return context.error(`Expected at least two arguments.`); + if (args.length < 2) { + return context.error(`Expected at least one argument.`); } - if ((args.length - 1) % 2 !== 0) { - return context.error(`Expected an even number of arguments.`); + const firstArg = args[1]; + if (!Array.isArray(firstArg) && typeof firstArg === 'object') { + return context.error(`First argument must be an image or text section.`); } const sections: Array = []; - for (let i = 1; i < args.length - 1; i += 2) { - const text = context.parse(args[i], 1, ValueType); - if (!text) return null; - const kind = text.type.kind; - if (kind !== 'string' && kind !== 'value' && kind !== 'null') - return context.error(`Formatted text type must be 'string', 'value', or 'null'.`); - - const options = (args[i + 1]: any); - if (typeof options !== "object" || Array.isArray(options)) - return context.error(`Format options argument must be an object.`); - - let scale = null; - if (options['font-scale']) { - scale = context.parse(options['font-scale'], 1, NumberType); - if (!scale) return null; + let nextTokenMayBeObject = false; + for (let i = 1; i <= args.length - 1; ++i) { + const arg = (args[i]: any); + + if (nextTokenMayBeObject && typeof arg === "object" && !Array.isArray(arg)) { + nextTokenMayBeObject = false; + + let scale = null; + if (arg['font-scale']) { + scale = context.parse(arg['font-scale'], 1, NumberType); + if (!scale) return null; + } + + let font = null; + if (arg['text-font']) { + font = context.parse(arg['text-font'], 1, array(StringType)); + if (!font) return null; + } + + let textColor = null; + if (arg['text-color']) { + textColor = context.parse(arg['text-color'], 1, ColorType); + if (!textColor) return null; + } + + const lastExpression = sections[sections.length - 1]; + lastExpression.scale = scale; + lastExpression.font = font; + lastExpression.textColor = textColor; + } else { + const content = context.parse(args[i], 1, ValueType); + if (!content) return null; + + const kind = content.type.kind; + if (kind !== 'string' && kind !== 'value' && kind !== 'null' && kind !== 'resolvedImage') + return context.error(`Formatted text type must be 'string', 'value', 'image' or 'null'.`); + + nextTokenMayBeObject = true; + sections.push({content, scale: null, font: null, textColor: null}); } - - let font = null; - if (options['text-font']) { - font = context.parse(options['text-font'], 1, array(StringType)); - if (!font) return null; - } - - let textColor = null; - if (options['text-color']) { - textColor = context.parse(options['text-color'], 1, ColorType); - if (!textColor) return null; - } - sections.push({text, scale, font, textColor}); } return new FormatExpression(sections); } evaluate(ctx: EvaluationContext) { - return new Formatted( - this.sections.map(section => - new FormattedSection( - toString(section.text.evaluate(ctx)), + const evaluateSection = section => { + const evaluatedContent = section.content.evaluate(ctx); + if (typeOf(evaluatedContent) === ResolvedImageType) { + return new FormattedSection('', evaluatedContent, null, null, null); + } + + return new FormattedSection( + toString(evaluatedContent), + null, section.scale ? section.scale.evaluate(ctx) : null, section.font ? section.font.evaluate(ctx).join(',') : null, section.textColor ? section.textColor.evaluate(ctx) : null - ) - ) - ); + ); + }; + + return new Formatted(this.sections.map(evaluateSection)); } eachChild(fn: (Expression) => void) { for (const section of this.sections) { - fn(section.text); + fn(section.content); if (section.scale) { fn(section.scale); } @@ -106,7 +126,7 @@ export default class FormatExpression implements Expression { serialize() { const serialized = ["format"]; for (const section of this.sections) { - serialized.push(section.text.serialize()); + serialized.push(section.content.serialize()); const options = {}; if (section.scale) { options['font-scale'] = section.scale.serialize(); diff --git a/src/style-spec/expression/types/formatted.js b/src/style-spec/expression/types/formatted.js index 9029a89527d..224594f5360 100644 --- a/src/style-spec/expression/types/formatted.js +++ b/src/style-spec/expression/types/formatted.js @@ -2,15 +2,18 @@ import {stringContainsRTLText} from "../../../util/script_detection"; import type Color from '../../util/color'; +import type ResolvedImage from '../types/resolved_image'; export class FormattedSection { text: string; + image: ResolvedImage | null; scale: number | null; fontStack: string | null; textColor: Color | null; - constructor(text: string, scale: number | null, fontStack: string | null, textColor: Color | null) { + constructor(text: string, image: ResolvedImage | null, scale: number | null, fontStack: string | null, textColor: Color | null) { this.text = text; + this.image = image; this.scale = scale; this.fontStack = fontStack; this.textColor = textColor; @@ -25,7 +28,13 @@ export default class Formatted { } static fromString(unformatted: string): Formatted { - return new Formatted([new FormattedSection(unformatted, null, null, null)]); + return new Formatted([new FormattedSection(unformatted, null, null, null, null)]); + } + + isEmpty(): boolean { + if (this.sections.length === 0) return true; + return !this.sections.some(section => section.text.length !== 0 || + (section.image && section.image.name.length !== 0)); } static factory(text: Formatted | string): Formatted { @@ -37,6 +46,7 @@ export default class Formatted { } toString(): string { + if (this.sections.length === 0) return ''; return this.sections.map(section => section.text).join(''); } @@ -52,6 +62,10 @@ export default class Formatted { serialize(): Array { const serialized = ["format"]; for (const section of this.sections) { + if (section.image) { + serialized.push(["image", section.image.name]); + continue; + } serialized.push(section.text); const options = {}; if (section.fontStack) { diff --git a/src/symbol/quads.js b/src/symbol/quads.js index 6d3b857f717..c30ea9a931f 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -6,9 +6,11 @@ import {GLYPH_PBF_BORDER} from '../style/parse_glyph_pbf'; import type Anchor from './anchor'; import type {PositionedIcon, Shaping} from './shaping'; +import {SHAPING_DEFAULT_OFFSET} from './shaping'; +import {IMAGE_PADDING} from '../render/image_atlas'; import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; import type {Feature} from '../style-spec/expression'; -import type {GlyphPosition} from '../render/glyph_atlas'; +import type {StyleImage} from '../style/style_image'; import ONE_EM from './one_em'; /** @@ -37,7 +39,8 @@ export type SymbolQuad = { }, writingMode: any | void, glyphOffset: [number, number], - sectionIndex: number + sectionIndex: number, + isSDF: boolean }; /** @@ -46,13 +49,14 @@ export type SymbolQuad = { */ export function getIconQuads( shapedIcon: PositionedIcon, - iconRotate: number): Array { + iconRotate: number, + isSDFIcon: boolean): Array { const image = shapedIcon.image; // 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; + const border = IMAGE_PADDING; // 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 @@ -86,7 +90,7 @@ export function getIconQuads( } // Icon quad is padded, so texture coordinates also need to be padded. - return [{tl, tr, bl, br, tex: image.paddedRect, writingMode: undefined, glyphOffset: [0, 0], sectionIndex: 0}]; + return [{tl, tr, bl, br, tex: image.paddedRect, writingMode: undefined, glyphOffset: [0, 0], sectionIndex: 0, isSDF: isSDFIcon}]; } /** @@ -99,93 +103,105 @@ export function getGlyphQuads(anchor: Anchor, layer: SymbolStyleLayer, alongLine: boolean, feature: Feature, - positions: {[string]: {[number]: GlyphPosition}}, + imageMap: {[string]: StyleImage}, allowVerticalPlacement: boolean): Array { const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}) * Math.PI / 180; - - const positionedGlyphs = shaping.positionedGlyphs; const quads = []; - for (let k = 0; k < positionedGlyphs.length; k++) { - const positionedGlyph = positionedGlyphs[k]; - const glyphPositions = positions[positionedGlyph.fontStack]; - const glyph = glyphPositions && glyphPositions[positionedGlyph.glyph]; - if (!glyph) continue; - - const rect = glyph.rect; - if (!rect) continue; - - // The rects have an addditional buffer that is not included in their size. - const glyphPadding = 1.0; - const rectBuffer = GLYPH_PBF_BORDER + glyphPadding; - - const halfAdvance = glyph.metrics.advance * positionedGlyph.scale / 2; - - const glyphOffset = alongLine ? - [positionedGlyph.x + halfAdvance, positionedGlyph.y] : - [0, 0]; - - let builtInOffset = alongLine ? - [0, 0] : - [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1]]; - - const rotateVerticalGlyph = (alongLine || allowVerticalPlacement) && positionedGlyph.vertical; - - let verticalizedLabelOffset = [0, 0]; - if (rotateVerticalGlyph) { - // Vertical POI labels that are rotated 90deg CW and whose glyphs must preserve upright orientation - // need to be rotated 90deg CCW. After a quad is rotated, it is translated to the original built-in offset. - verticalizedLabelOffset = builtInOffset; - builtInOffset = [0, 0]; - } - - const x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0]; - const y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1]; - const x2 = x1 + rect.w * positionedGlyph.scale; - const y2 = y1 + rect.h * positionedGlyph.scale; - - const tl = new Point(x1, y1); - const tr = new Point(x2, y1); - const bl = new Point(x1, y2); - const br = new Point(x2, y2); - - if (rotateVerticalGlyph) { - // Vertical-supporting glyphs are laid out in 24x24 point boxes (1 square em) - // In horizontal orientation, the y values for glyphs are below the midline - // and we use a "yOffset" of -17 to pull them up to the middle. - // By rotating counter-clockwise around the point at the center of the left - // edge of a 24x24 layout box centered below the midline, we align the center - // of the glyphs with the horizontal midline, so the yOffset is no longer - // necessary, but we also pull the glyph to the left along the x axis. - // The y coordinate includes baseline yOffset, thus needs to be accounted - // for when glyph is rotated and translated. - const center = new Point(-halfAdvance, halfAdvance - shaping.yOffset); - const verticalRotation = -Math.PI / 2; - - // xHalfWidhtOffsetcorrection is a difference between full-width and half-width - // advance, should be 0 for full-width glyphs and will pull up half-width glyphs. - const xHalfWidhtOffsetcorrection = ONE_EM / 2 - halfAdvance; - const xOffsetCorrection = new Point(5 - shaping.yOffset - xHalfWidhtOffsetcorrection, 0); - const verticalOffsetCorrection = new Point(...verticalizedLabelOffset); - tl._rotateAround(verticalRotation, center)._add(xOffsetCorrection)._add(verticalOffsetCorrection); - tr._rotateAround(verticalRotation, center)._add(xOffsetCorrection)._add(verticalOffsetCorrection); - bl._rotateAround(verticalRotation, center)._add(xOffsetCorrection)._add(verticalOffsetCorrection); - br._rotateAround(verticalRotation, center)._add(xOffsetCorrection)._add(verticalOffsetCorrection); + for (const line of shaping.positionedLines) { + for (const positionedGlyph of line.positionedGlyphs) { + if (!positionedGlyph.rect) continue; + const textureRect = positionedGlyph.rect || {}; + + // The rects have an additional buffer that is not included in their size. + const glyphPadding = 1.0; + let rectBuffer = GLYPH_PBF_BORDER + glyphPadding; + let isSDF = true; + let pixelRatio = 1.0; + let lineOffset = 0.0; + + const rotateVerticalGlyph = (alongLine || allowVerticalPlacement) && positionedGlyph.vertical; + const halfAdvance = positionedGlyph.metrics.advance * positionedGlyph.scale / 2; + + // Align images and scaled glyphs in the middle of a vertical line. + if (allowVerticalPlacement && shaping.verticalizable) { + const scaledGlyphOffset = (positionedGlyph.scale - 1) * ONE_EM; + const imageOffset = (ONE_EM - positionedGlyph.metrics.width * positionedGlyph.scale) / 2; + lineOffset = line.lineOffset / 2 - (positionedGlyph.imageName ? -imageOffset : scaledGlyphOffset); + } + + if (positionedGlyph.imageName) { + const image = imageMap[positionedGlyph.imageName]; + isSDF = image.sdf; + pixelRatio = image.pixelRatio; + rectBuffer = IMAGE_PADDING / pixelRatio; + } + + const glyphOffset = alongLine ? + [positionedGlyph.x + halfAdvance, positionedGlyph.y] : + [0, 0]; + + let builtInOffset = alongLine ? + [0, 0] : + [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1] - lineOffset]; + + let verticalizedLabelOffset = [0, 0]; + if (rotateVerticalGlyph) { + // Vertical POI labels that are rotated 90deg CW and whose glyphs must preserve upright orientation + // need to be rotated 90deg CCW. After a quad is rotated, it is translated to the original built-in offset. + verticalizedLabelOffset = builtInOffset; + builtInOffset = [0, 0]; + } + + const x1 = (positionedGlyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0]; + const y1 = (-positionedGlyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1]; + const x2 = x1 + textureRect.w * positionedGlyph.scale / pixelRatio; + const y2 = y1 + textureRect.h * positionedGlyph.scale / pixelRatio; + + const tl = new Point(x1, y1); + const tr = new Point(x2, y1); + const bl = new Point(x1, y2); + const br = new Point(x2, y2); + + if (rotateVerticalGlyph) { + // Vertical-supporting glyphs are laid out in 24x24 point boxes (1 square em) + // In horizontal orientation, the y values for glyphs are below the midline + // and we use a "yOffset" of -17 to pull them up to the middle. + // By rotating counter-clockwise around the point at the center of the left + // edge of a 24x24 layout box centered below the midline, we align the center + // of the glyphs with the horizontal midline, so the yOffset is no longer + // necessary, but we also pull the glyph to the left along the x axis. + // The y coordinate includes baseline yOffset, thus needs to be accounted + // for when glyph is rotated and translated. + const center = new Point(-halfAdvance, halfAdvance - SHAPING_DEFAULT_OFFSET); + const verticalRotation = -Math.PI / 2; + + // xHalfWidhtOffsetCorrection is a difference between full-width and half-width + // advance, should be 0 for full-width glyphs and will pull up half-width glyphs. + const xHalfWidhtOffsetCorrection = ONE_EM / 2 - halfAdvance; + const yImageOffsetCorrection = positionedGlyph.imageName ? xHalfWidhtOffsetCorrection : 0.0; + const halfWidhtOffsetCorrection = new Point(5 - SHAPING_DEFAULT_OFFSET - xHalfWidhtOffsetCorrection, -yImageOffsetCorrection); + const verticalOffsetCorrection = new Point(...verticalizedLabelOffset); + tl._rotateAround(verticalRotation, center)._add(halfWidhtOffsetCorrection)._add(verticalOffsetCorrection); + tr._rotateAround(verticalRotation, center)._add(halfWidhtOffsetCorrection)._add(verticalOffsetCorrection); + bl._rotateAround(verticalRotation, center)._add(halfWidhtOffsetCorrection)._add(verticalOffsetCorrection); + br._rotateAround(verticalRotation, center)._add(halfWidhtOffsetCorrection)._add(verticalOffsetCorrection); + } + + if (textRotate) { + const sin = Math.sin(textRotate), + cos = Math.cos(textRotate), + matrix = [cos, -sin, sin, cos]; + + tl._matMult(matrix); + tr._matMult(matrix); + bl._matMult(matrix); + br._matMult(matrix); + } + + quads.push({tl, tr, bl, br, tex: textureRect, writingMode: shaping.writingMode, glyphOffset, sectionIndex: positionedGlyph.sectionIndex, isSDF}); } - - if (textRotate) { - const sin = Math.sin(textRotate), - cos = Math.cos(textRotate), - matrix = [cos, -sin, sin, cos]; - - tl._matMult(matrix); - tr._matMult(matrix); - bl._matMult(matrix); - br._matMult(matrix); - } - - quads.push({tl, tr, bl, br, tex: rect, writingMode: shaping.writingMode, glyphOffset, sectionIndex: positionedGlyph.sectionIndex}); } return quads; diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index 0c95738fb87..47541b98c16 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -9,10 +9,14 @@ import { import verticalizePunctuation from '../util/verticalize_punctuation'; import {plugin as rtlTextPlugin} from '../source/rtl_text_plugin'; import ONE_EM from './one_em'; +import {warnOnce} from '../util/util'; -import type {StyleGlyph} from '../style/style_glyph'; +import type {StyleGlyph, GlyphMetrics} from '../style/style_glyph'; +import {GLYPH_PBF_BORDER} from '../style/parse_glyph_pbf'; import type {ImagePosition} from '../render/image_atlas'; -import Formatted from '../style-spec/expression/types/formatted'; +import {IMAGE_PADDING} from '../render/image_atlas'; +import type {Rect, GlyphPosition} from '../render/glyph_atlas'; +import Formatted, {FormattedSection} from '../style-spec/expression/types/formatted'; const WritingMode = { horizontal: 1, @@ -20,57 +24,107 @@ const WritingMode = { horizontalOnly: 3 }; -export {shapeText, shapeIcon, fitIconToText, getAnchorAlignment, WritingMode}; +const SHAPING_DEFAULT_OFFSET = -17; +export {shapeText, shapeIcon, fitIconToText, getAnchorAlignment, WritingMode, SHAPING_DEFAULT_OFFSET}; // The position of a glyph relative to the text's anchor point. export type PositionedGlyph = { glyph: number, + imageName: string | null, x: number, y: number, vertical: boolean, scale: number, fontStack: string, - sectionIndex: number + sectionIndex: number, + metrics: GlyphMetrics, + rect: Rect | null +}; + +export type PositionedLine = { + positionedGlyphs: Array, + lineOffset: number }; // A collection of positioned glyphs and some metadata export type Shaping = { - positionedGlyphs: Array, + positionedLines: Array, top: number, bottom: number, left: number, right: number, writingMode: 1 | 2, - lineCount: number, text: string, - yOffset: number, + iconsInText: boolean, + verticalizable: boolean }; +function isEmpty(positionedLines: Array) { + for (const line of positionedLines) { + if (line.positionedGlyphs.length !== 0) { + return false; + } + } + return true; +} + export type SymbolAnchor = 'center' | 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; export type TextJustify = 'left' | 'center' | 'right'; +// Max number of images in label is 6401 U+E000–U+F8FF that covers +// Basic Multilingual Plane Unicode Private Use Area (PUA). +const PUAbegin = 0xE000; +const PUAend = 0xF8FF; + +class SectionOptions { + // Text options + scale: number; + fontStack: string; + // Image options + imageName: string | null; + + constructor() { + this.scale = 1.0; + this.fontStack = ""; + this.imageName = null; + } + + static forText(scale: number | null, fontStack: string) { + const textOptions = new SectionOptions(); + textOptions.scale = scale || 1; + textOptions.fontStack = fontStack; + return textOptions; + } + + static forImage(imageName: string) { + const imageOptions = new SectionOptions(); + imageOptions.imageName = imageName; + return imageOptions; + } + +} + class TaggedString { text: string; sectionIndex: Array // maps each character in 'text' to its corresponding entry in 'sections' - sections: Array<{ scale: number, fontStack: string }> + sections: Array + imageSectionID: number | null; constructor() { this.text = ""; this.sectionIndex = []; this.sections = []; + this.imageSectionID = null; } static fromFeature(text: Formatted, defaultFontStack: string) { const result = new TaggedString(); for (let i = 0; i < text.sections.length; i++) { const section = text.sections[i]; - result.sections.push({ - scale: section.scale || 1, - fontStack: section.fontStack || defaultFontStack - }); - result.text += section.text; - for (let j = 0; j < section.text.length; j++) { - result.sectionIndex.push(i); + if (!section.image) { + result.addTextSection(section, defaultFontStack); + } else { + result.addImageSection(section); } } return result; @@ -80,7 +134,7 @@ class TaggedString { return this.text.length; } - getSection(index: number): { scale: number, fontStack: string } { + getSection(index: number): SectionOptions { return this.sections[this.sectionIndex[index]]; } @@ -128,6 +182,43 @@ class TaggedString { getMaxScale() { return this.sectionIndex.reduce((max, index) => Math.max(max, this.sections[index].scale), 0); } + + addTextSection(section: FormattedSection, defaultFontStack: string) { + this.text += section.text; + this.sections.push(SectionOptions.forText(section.scale, section.fontStack || defaultFontStack)); + const index = this.sections.length - 1; + for (let i = 0; i < section.text.length; ++i) { + this.sectionIndex.push(index); + } + } + + addImageSection(section: FormattedSection) { + const imageName = section.image ? section.image.name : ''; + if (imageName.length === 0) { + warnOnce(`Can't add FormattedSection with an empty image.`); + return; + } + + const nextImageSectionCharCode = this.getNextImageSectionCharCode(); + if (!nextImageSectionCharCode) { + warnOnce(`Reached maximum number of images ${PUAend - PUAbegin + 2}`); + return; + } + + this.text += String.fromCharCode(nextImageSectionCharCode); + this.sections.push(SectionOptions.forImage(imageName)); + this.sectionIndex.push(this.sections.length - 1); + } + + getNextImageSectionCharCode(): number | null { + if (!this.imageSectionID) { + this.imageSectionID = PUAbegin; + return this.imageSectionID; + } + + if (this.imageSectionID >= PUAend) return null; + return ++this.imageSectionID; + } } function breakLines(input: TaggedString, lineBreakPoints: Array): Array { @@ -146,7 +237,9 @@ function breakLines(input: TaggedString, lineBreakPoints: Array): Array< } function shapeText(text: Formatted, - glyphs: {[string]: {[number]: ?StyleGlyph}}, + glyphMap: {[string]: {[number]: ?StyleGlyph}}, + glyphPositions: {[string]: {[number]: GlyphPosition}}, + imagePositions: {[string]: ImagePosition}, defaultFontStack: string, maxWidth: number, lineHeight: number, @@ -156,7 +249,9 @@ function shapeText(text: Formatted, translate: [number, number], writingMode: 1 | 2, allowVerticalPlacement: boolean, - symbolPlacement: string): Shaping | false { + symbolPlacement: string, + layoutTextSize: number, + layoutTextSizeThisZoom: number): Shaping | false { const logicalInput = TaggedString.fromFeature(text, defaultFontStack); if (writingMode === WritingMode.vertical) { @@ -171,7 +266,7 @@ function shapeText(text: Formatted, lines = []; const untaggedLines = processBidirectionalText(logicalInput.toString(), - determineLineBreaks(logicalInput, spacing, maxWidth, glyphs, symbolPlacement)); + determineLineBreaks(logicalInput, spacing, maxWidth, glyphMap, imagePositions, symbolPlacement, layoutTextSize)); for (const line of untaggedLines) { const taggedLine = new TaggedString(); taggedLine.text = line; @@ -188,7 +283,7 @@ function shapeText(text: Formatted, const processedLines = processStyledBidirectionalText(logicalInput.text, logicalInput.sectionIndex, - determineLineBreaks(logicalInput, spacing, maxWidth, glyphs, symbolPlacement)); + determineLineBreaks(logicalInput, spacing, maxWidth, glyphMap, imagePositions, symbolPlacement, layoutTextSize)); for (const line of processedLines) { const taggedLine = new TaggedString(); taggedLine.text = line[0]; @@ -197,24 +292,24 @@ function shapeText(text: Formatted, lines.push(taggedLine); } } else { - lines = breakLines(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphs, symbolPlacement)); + lines = breakLines(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphMap, imagePositions, symbolPlacement, layoutTextSize)); } - const positionedGlyphs = []; + const positionedLines = []; const shaping = { - positionedGlyphs, + positionedLines, text: logicalInput.toString(), top: translate[1], bottom: translate[1], left: translate[0], right: translate[0], writingMode, - lineCount: lines.length, - yOffset: -17 // the y offset *should* be part of the font metadata + iconsInText: false, + verticalizable: false }; - shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify, writingMode, spacing, allowVerticalPlacement); - if (!positionedGlyphs.length) return false; + shapeLines(shaping, glyphMap, glyphPositions, imagePositions, lines, lineHeight, textAnchor, textJustify, writingMode, spacing, allowVerticalPlacement, layoutTextSizeThisZoom); + if (isEmpty(positionedLines)) return false; return shaping; } @@ -251,19 +346,35 @@ const breakable: {[number]: boolean} = { // See https://github.com/mapbox/mapbox-gl-js/issues/3658 }; +function getGlyphAdvance(codePoint: number, + section: SectionOptions, + glyphMap: {[string]: {[number]: ?StyleGlyph}}, + imagePositions: {[string]: ImagePosition}, + spacing: number, + layoutTextSize: number): number { + if (!section.imageName) { + const positions = glyphMap[section.fontStack]; + const glyph = positions && positions[codePoint]; + if (!glyph) return 0; + return glyph.metrics.advance * section.scale + spacing; + } else { + const imagePosition = imagePositions[section.imageName]; + if (!imagePosition) return 0; + return imagePosition.displaySize[0] * section.scale * ONE_EM / layoutTextSize + spacing; + } +} + function determineAverageLineWidth(logicalInput: TaggedString, spacing: number, maxWidth: number, - glyphMap: {[string]: {[number]: ?StyleGlyph}}) { + glyphMap: {[string]: {[number]: ?StyleGlyph}}, + imagePositions: {[string]: ImagePosition}, + layoutTextSize: number) { let totalWidth = 0; for (let index = 0; index < logicalInput.length(); index++) { const section = logicalInput.getSection(index); - const positions = glyphMap[section.fontStack]; - const glyph = positions && positions[logicalInput.getCharCode(index)]; - if (!glyph) - continue; - totalWidth += glyph.metrics.advance * section.scale + spacing; + totalWidth += getGlyphAdvance(logicalInput.getCharCode(index), section, glyphMap, imagePositions, spacing, layoutTextSize); } const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth)); @@ -361,7 +472,9 @@ function determineLineBreaks(logicalInput: TaggedString, spacing: number, maxWidth: number, glyphMap: {[string]: {[number]: ?StyleGlyph}}, - symbolPlacement: string): Array { + imagePositions: {[string]: ImagePosition}, + symbolPlacement: string, + layoutTextSize: number): Array { if (symbolPlacement !== 'point') return []; @@ -369,7 +482,7 @@ function determineLineBreaks(logicalInput: TaggedString, return []; const potentialLineBreaks = []; - const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap); + const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap, imagePositions, layoutTextSize); const hasServerSuggestedBreakpoints = logicalInput.text.indexOf("\u200b") >= 0; @@ -378,17 +491,13 @@ function determineLineBreaks(logicalInput: TaggedString, for (let i = 0; i < logicalInput.length(); i++) { const section = logicalInput.getSection(i); const codePoint = logicalInput.getCharCode(i); - const positions = glyphMap[section.fontStack]; - const glyph = positions && positions[codePoint]; - - if (glyph && !whitespace[codePoint]) - currentX += glyph.metrics.advance * section.scale + spacing; + if (!whitespace[codePoint]) currentX += getGlyphAdvance(codePoint, section, glyphMap, imagePositions, spacing, layoutTextSize); // Ideographic characters, spaces, and word-breaking punctuation that often appear without // surrounding spaces. if ((i < logicalInput.length() - 1)) { const ideographicBreak = charAllowsIdeographicBreaking(codePoint); - if (breakable[codePoint] || ideographicBreak) { + if (breakable[codePoint] || ideographicBreak || section.imageName) { potentialLineBreaks.push( evaluateBreak( @@ -446,79 +555,139 @@ function getAnchorAlignment(anchor: SymbolAnchor) { function shapeLines(shaping: Shaping, glyphMap: {[string]: {[number]: ?StyleGlyph}}, + glyphPositions: {[string]: {[number]: GlyphPosition}}, + imagePositions: {[string]: ImagePosition}, lines: Array, lineHeight: number, textAnchor: SymbolAnchor, textJustify: TextJustify, writingMode: 1 | 2, spacing: number, - allowVerticalPlacement: boolean) { + allowVerticalPlacement: boolean, + layoutTextSizeThisZoom: number) { let x = 0; - let y = shaping.yOffset; + let y = SHAPING_DEFAULT_OFFSET; let maxLineLength = 0; - const positionedGlyphs = shaping.positionedGlyphs; + let maxLineHeight = 0; const justify = textJustify === 'right' ? 1 : textJustify === 'left' ? 0 : 0.5; + let lineIndex = 0; for (const line of lines) { line.trim(); const lineMaxScale = line.getMaxScale(); + const maxLineOffset = (lineMaxScale - 1) * ONE_EM; + const positionedLine = {positionedGlyphs: [], lineOffset: 0}; + shaping.positionedLines[lineIndex] = positionedLine; + const positionedGlyphs = positionedLine.positionedGlyphs; + let lineOffset = 0.0; if (!line.length()) { y += lineHeight; // Still need a line feed after empty line + ++lineIndex; continue; } - const lineStartIndex = positionedGlyphs.length; for (let i = 0; i < line.length(); i++) { const section = line.getSection(i); const sectionIndex = line.getSectionIndex(i); const codePoint = line.getCharCode(i); - // We don't know the baseline, but since we're laying out - // at 24 points, we can calculate how much it will move when - // we scale up or down. - const baselineOffset = (lineMaxScale - section.scale) * 24; - const positions = glyphMap[section.fontStack]; - const glyph = positions && positions[codePoint]; - - if (!glyph) continue; - - if (writingMode === WritingMode.horizontal || + let baselineOffset = 0.0; + let metrics = null; + let rect = null; + let imageName = null; + let verticalAdvance = ONE_EM; + const vertical = !(writingMode === WritingMode.horizontal || // Don't verticalize glyphs that have no upright orientation if vertical placement is disabled. (!allowVerticalPlacement && !charHasUprightVerticalOrientation(codePoint)) || - // If vertical placement is ebabled, don't verticalize glyphs that + // If vertical placement is enabled, don't verticalize glyphs that // are from complex text layout script, or whitespaces. - (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))) { - positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: false, scale: section.scale, fontStack: section.fontStack, sectionIndex}); - x += glyph.metrics.advance * section.scale + spacing; + (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))); + + if (!section.imageName) { + const positions = glyphPositions[section.fontStack]; + const glyphPosition = positions && positions[codePoint]; + if (glyphPosition && glyphPosition.rect) { + rect = glyphPosition.rect; + metrics = glyphPosition.metrics; + } else { + const glyphs = glyphMap[section.fontStack]; + const glyph = glyphs && glyphs[codePoint]; + if (!glyph) continue; + metrics = glyph.metrics; + } + + // We don't know the baseline, but since we're laying out + // at 24 points, we can calculate how much it will move when + // we scale up or down. + baselineOffset = (lineMaxScale - section.scale) * ONE_EM; } else { - positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: true, scale: section.scale, fontStack: section.fontStack, sectionIndex}); - x += ONE_EM * section.scale + spacing; + const imagePosition = imagePositions[section.imageName]; + if (!imagePosition) continue; + imageName = section.imageName; + shaping.iconsInText = shaping.iconsInText || true; + rect = imagePosition.paddedRect; + const size = imagePosition.displaySize; + // If needed, allow to set scale factor for an image using + // alias "image-scale" that could be alias for "font-scale" + // when FormattedSection is an image section. + section.scale = section.scale * ONE_EM / layoutTextSizeThisZoom; + + metrics = {width: size[0], + height: size[1], + left: IMAGE_PADDING, + top: -GLYPH_PBF_BORDER, + advance: vertical ? size[1] : size[0]}; + + // Difference between one EM and an image size. + // Aligns bottom of an image to a baseline level. + const imageOffset = ONE_EM - size[1] * section.scale; + baselineOffset = maxLineOffset + imageOffset; + verticalAdvance = metrics.advance; + + // Difference between height of an image and one EM at max line scale. + // Pushes current line down if an image size is over 1 EM at max line scale. + const offset = vertical ? size[0] * section.scale - ONE_EM * lineMaxScale : + size[1] * section.scale - ONE_EM * lineMaxScale; + if (offset > 0 && offset > lineOffset) { + lineOffset = offset; + } + } + + if (!vertical) { + positionedGlyphs.push({glyph: codePoint, imageName, x, y: y + baselineOffset, vertical, scale: section.scale, fontStack: section.fontStack, sectionIndex, metrics, rect}); + x += metrics.advance * section.scale + spacing; + } else { + shaping.verticalizable = true; + positionedGlyphs.push({glyph: codePoint, imageName, x, y: y + baselineOffset, vertical, scale: section.scale, fontStack: section.fontStack, sectionIndex, metrics, rect}); + x += verticalAdvance * section.scale + spacing; } } // Only justify if we placed at least one glyph - if (positionedGlyphs.length !== lineStartIndex) { + if (positionedGlyphs.length !== 0) { const lineLength = x - spacing; maxLineLength = Math.max(lineLength, maxLineLength); - - justifyLine(positionedGlyphs, glyphMap, lineStartIndex, positionedGlyphs.length - 1, justify); + justifyLine(positionedGlyphs, 0, positionedGlyphs.length - 1, justify, lineOffset); } x = 0; - y += lineHeight * lineMaxScale; + const currentLineHeight = lineHeight * lineMaxScale + lineOffset; + positionedLine.lineOffset = Math.max(lineOffset, maxLineOffset); + y += currentLineHeight; + maxLineHeight = Math.max(currentLineHeight, maxLineHeight); + ++lineIndex; } + // Calculate the bounding box and justify / align text block. + const height = y - SHAPING_DEFAULT_OFFSET; const {horizontalAlign, verticalAlign} = getAnchorAlignment(textAnchor); - align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, lines.length); - - // Calculate the bounding box - const height = y - shaping.yOffset; + align(shaping.positionedLines, justify, horizontalAlign, verticalAlign, maxLineLength, maxLineHeight, lineHeight, height, lines.length); shaping.top += -verticalAlign * height; shaping.bottom = shaping.top + height; @@ -528,39 +697,46 @@ function shapeLines(shaping: Shaping, // justify right = 1, left = 0, center = 0.5 function justifyLine(positionedGlyphs: Array, - glyphMap: {[string]: {[number]: ?StyleGlyph}}, start: number, end: number, - justify: 1 | 0 | 0.5) { - if (!justify) + justify: 1 | 0 | 0.5, + lineOffset: number) { + if (!justify && !lineOffset) return; const lastPositionedGlyph = positionedGlyphs[end]; - const positions = glyphMap[lastPositionedGlyph.fontStack]; - const glyph = positions && positions[lastPositionedGlyph.glyph]; - if (glyph) { - const lastAdvance = glyph.metrics.advance * lastPositionedGlyph.scale; - const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; - - for (let j = start; j <= end; j++) { - positionedGlyphs[j].x -= lineIndent; - } + const lastAdvance = lastPositionedGlyph.metrics.advance * lastPositionedGlyph.scale; + const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; + + for (let j = start; j <= end; j++) { + positionedGlyphs[j].x -= lineIndent; + positionedGlyphs[j].y += lineOffset; } } -function align(positionedGlyphs: Array, +function align(positionedLines: Array, justify: number, horizontalAlign: number, verticalAlign: number, maxLineLength: number, + maxLineHeight: number, lineHeight: number, + blockHeight: number, lineCount: number) { const shiftX = (justify - horizontalAlign) * maxLineLength; - const shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight; + let shiftY = 0; - for (let j = 0; j < positionedGlyphs.length; j++) { - positionedGlyphs[j].x += shiftX; - positionedGlyphs[j].y += shiftY; + if (maxLineHeight !== lineHeight) { + shiftY = -blockHeight * verticalAlign - SHAPING_DEFAULT_OFFSET; + } else { + shiftY = (-verticalAlign * lineCount + 0.5) * lineHeight; + } + + for (const line of positionedLines) { + for (const positionedGlyph of line.positionedGlyphs) { + positionedGlyph.x += shiftX; + positionedGlyph.y += shiftY; + } } } diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index f07d1a04348..28c182ba5c8 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -188,10 +188,13 @@ export function performSymbolLayout(bucket: SymbolBucket, const lineHeight = layout.get('text-line-height') * ONE_EM; const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point'; const keepUpright = layout.get('text-keep-upright'); + const textSize = layout.get('text-size'); for (const feature of bucket.features) { const fontstack = layout.get('text-font').evaluate(feature, {}).join(','); - const glyphPositionMap = glyphPositions; + const layoutTextSizeThisZoom = textSize.evaluate(feature, {}); + const layoutTextSize = sizes.layoutTextSize.evaluate(feature, {}); + const layoutIconSize = sizes.layoutIconSize.evaluate(feature, {}); const shapedTextOrientations = { horizontal: {}, @@ -234,8 +237,8 @@ export function performSymbolLayout(bucket: SymbolBucket, // Vertical POI label placement is meant to be used for scripts that support vertical // writing mode, thus, default left justification is used. If Latin // scripts would need to be supported, this should take into account other justifications. - shapedTextOrientations.vertical = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, textAnchor, - 'left', spacingIfAllowed, textOffset, WritingMode.vertical, true, symbolPlacement); + shapedTextOrientations.vertical = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, + 'left', spacingIfAllowed, textOffset, WritingMode.vertical, true, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); } }; @@ -256,11 +259,11 @@ export function performSymbolLayout(bucket: SymbolBucket, } else { // If using text-variable-anchor for the layer, we use a center anchor for all shapings and apply // the offsets for the anchor in the placement step. - const shaping = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, 'center', - justification, spacingIfAllowed, textOffset, WritingMode.horizontal, false, symbolPlacement); + const shaping = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, 'center', + justification, spacingIfAllowed, textOffset, WritingMode.horizontal, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); if (shaping) { shapedTextOrientations.horizontal[justification] = shaping; - singleLine = shaping.lineCount === 1; + singleLine = shaping.positionedLines.length === 1; } } } @@ -272,8 +275,8 @@ export function performSymbolLayout(bucket: SymbolBucket, } // Horizontal point or line label. - const shaping = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, - textOffset, WritingMode.horizontal, false, symbolPlacement); + const shaping = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, + textOffset, WritingMode.horizontal, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); if (shaping) shapedTextOrientations.horizontal[textJustify] = shaping; // Vertical point label (if allowVerticalPlacement is enabled). @@ -281,14 +284,14 @@ export function performSymbolLayout(bucket: SymbolBucket, // Verticalized line label. if (allowsVerticalWritingMode(unformattedText) && textAlongLine && keepUpright) { - shapedTextOrientations.vertical = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, textAnchor, textJustify, - spacingIfAllowed, textOffset, WritingMode.vertical, false, symbolPlacement); + shapedTextOrientations.vertical = shapeText(text, glyphMap, glyphPositions, imagePositions, fontstack, maxWidth, lineHeight, textAnchor, textJustify, + spacingIfAllowed, textOffset, WritingMode.vertical, false, symbolPlacement, layoutTextSize, layoutTextSizeThisZoom); } } - } let shapedIcon; + let isSDFIcon = false; if (feature.icon && feature.icon.name) { const image = imageMap[feature.icon.name]; if (image) { @@ -296,6 +299,7 @@ export function performSymbolLayout(bucket: SymbolBucket, imagePositions[feature.icon.name], layout.get('icon-offset').evaluate(feature, {}), layout.get('icon-anchor').evaluate(feature, {})); + isSDFIcon = image.sdf; if (bucket.sdfIcons === undefined) { bucket.sdfIcons = image.sdf; } else if (bucket.sdfIcons !== image.sdf) { @@ -309,8 +313,10 @@ export function performSymbolLayout(bucket: SymbolBucket, } } - if (Object.keys(shapedTextOrientations.horizontal).length || shapedIcon) { - addFeature(bucket, feature, shapedTextOrientations, shapedIcon, glyphPositionMap, sizes, textOffset); + const shapedText = getDefaultHorizontalShaping(shapedTextOrientations.horizontal) || shapedTextOrientations.vertical; + bucket.iconsInText = shapedText ? shapedText.iconsInText : false; + if (shapedText || shapedIcon) { + addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon); } } @@ -345,12 +351,12 @@ function addFeature(bucket: SymbolBucket, feature: SymbolFeature, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, - glyphPositionMap: {[string]: {[number]: GlyphPosition}}, + imageMap: {[string]: StyleImage}, sizes: Sizes, - textOffset: [number, number]) { - const layoutTextSize = sizes.layoutTextSize.evaluate(feature, {}); - const layoutIconSize = sizes.layoutIconSize.evaluate(feature, {}); - + layoutTextSize: number, + layoutIconSize: number, + textOffset: [number, number], + isSDFIcon: boolean) { // To reduce the number of labels that jump around when zooming we need // to use a text-size value that is the same for all zoom levels. // bucket calculates text-size at a high zoom level so that all tiles can @@ -399,11 +405,11 @@ function addFeature(bucket: SymbolBucket, return; } - addSymbol(bucket, anchor, line, shapedTextOrientations, shapedIcon, verticallyShapedIcon, bucket.layers[0], + addSymbol(bucket, anchor, line, shapedTextOrientations, shapedIcon, imageMap, verticallyShapedIcon, bucket.layers[0], bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex, bucket.index, textBoxScale, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, - feature, glyphPositionMap, sizes); + feature, sizes, isSDFIcon); }; if (symbolPlacement === 'line') { @@ -463,11 +469,14 @@ function addFeature(bucket: SymbolBucket, } } -const MAX_PACKED_SIZE = 65535; +const MAX_GLYPH_ICON_SIZE = 255; +const MAX_PACKED_SIZE = MAX_GLYPH_ICON_SIZE * SIZE_PACK_FACTOR; +export {MAX_PACKED_SIZE}; function addTextVertices(bucket: SymbolBucket, anchor: Point, shapedText: Shaping, + imageMap: {[string]: StyleImage}, layer: SymbolStyleLayer, textAlongLine: boolean, feature: SymbolFeature, @@ -476,11 +485,10 @@ function addTextVertices(bucket: SymbolBucket, writingMode: number, placementTypes: Array<'vertical' | 'center' | 'left' | 'right'>, placedTextSymbolIndices: {[string]: number}, - glyphPositionMap: {[string]: {[number]: GlyphPosition}}, placedIconIndex: number, sizes: Sizes) { const glyphQuads = getGlyphQuads(anchor, shapedText, textOffset, - layer, textAlongLine, feature, glyphPositionMap, bucket.allowVerticalPlacement); + layer, textAlongLine, feature, imageMap, bucket.allowVerticalPlacement); const sizeData = bucket.textSizeData; let textSizeData = null; @@ -490,7 +498,7 @@ function addTextVertices(bucket: SymbolBucket, SIZE_PACK_FACTOR * layer.layout.get('text-size').evaluate(feature, {}) ]; if (textSizeData[0] > MAX_PACKED_SIZE) { - warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= 256. Reduce your "text-size".`); + warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`); } } else if (sizeData.kind === 'composite') { textSizeData = [ @@ -498,7 +506,7 @@ function addTextVertices(bucket: SymbolBucket, SIZE_PACK_FACTOR * sizes.compositeTextSizes[1].evaluate(feature, {}) ]; if (textSizeData[0] > MAX_PACKED_SIZE || textSizeData[1] > MAX_PACKED_SIZE) { - warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= 256. Reduce your "text-size".`); + warnOnce(`${bucket.layerIds[0]}: Value for "text-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "text-size".`); } } @@ -543,6 +551,7 @@ function addSymbol(bucket: SymbolBucket, line: Array, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, + imageMap: {[string]: StyleImage}, verticallyShapedIcon: PositionedIcon | void, layer: SymbolStyleLayer, collisionBoxArray: CollisionBoxArray, @@ -558,8 +567,8 @@ function addSymbol(bucket: SymbolBucket, iconAlongLine: boolean, iconOffset: [number, number], feature: SymbolFeature, - glyphPositionMap: {[string]: {[number]: GlyphPosition}}, - sizes: Sizes) { + sizes: Sizes, + isSDFIcon: boolean) { const lineArray = bucket.addToLineVertexArray(anchor, line); let textCollisionFeature, iconCollisionFeature, verticalTextCollisionFeature, verticalIconCollisionFeature; @@ -599,8 +608,8 @@ function addSymbol(bucket: SymbolBucket, // For more info check `updateVariableAnchors` in `draw_symbol.js` . if (shapedIcon) { const iconRotate = layer.layout.get('icon-rotate').evaluate(feature, {}); - const iconQuads = getIconQuads(shapedIcon, iconRotate); - const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate) : undefined; + const iconQuads = getIconQuads(shapedIcon, iconRotate, isSDFIcon); + const verticalIconQuads = verticallyShapedIcon ? getIconQuads(verticallyShapedIcon, iconRotate, isSDFIcon) : undefined; iconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, /*align boxes to line*/false, bucket.overscaling, iconRotate); numIconVertices = iconQuads.length * 4; @@ -613,7 +622,7 @@ function addSymbol(bucket: SymbolBucket, SIZE_PACK_FACTOR * layer.layout.get('icon-size').evaluate(feature, {}) ]; if (iconSizeData[0] > MAX_PACKED_SIZE) { - warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= 256. Reduce your "icon-size".`); + warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`); } } else if (sizeData.kind === 'composite') { iconSizeData = [ @@ -621,7 +630,7 @@ function addSymbol(bucket: SymbolBucket, SIZE_PACK_FACTOR * sizes.compositeIconSizes[1].evaluate(feature, {}) ]; if (iconSizeData[0] > MAX_PACKED_SIZE || iconSizeData[1] > MAX_PACKED_SIZE) { - warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= 256. Reduce your "icon-size".`); + warnOnce(`${bucket.layerIds[0]}: Value for "icon-size" is >= ${MAX_GLYPH_ICON_SIZE}. Reduce your "icon-size".`); } } @@ -673,12 +682,12 @@ function addSymbol(bucket: SymbolBucket, textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaping, textBoxScale, textPadding, textAlongLine, bucket.overscaling, textRotate); } - const singleLine = shaping.lineCount === 1; + const singleLine = shaping.positionedLines.length === 1; numHorizontalGlyphVertices += addTextVertices( - bucket, anchor, shaping, layer, textAlongLine, feature, textOffset, lineArray, + bucket, anchor, shaping, imageMap, layer, textAlongLine, feature, textOffset, lineArray, shapedTextOrientations.vertical ? WritingMode.horizontal : WritingMode.horizontalOnly, singleLine ? (Object.keys(shapedTextOrientations.horizontal): any) : [justification], - placedTextSymbolIndices, glyphPositionMap, placedIconSymbolIndex, sizes); + placedTextSymbolIndices, placedIconSymbolIndex, sizes); if (singleLine) { break; @@ -687,8 +696,8 @@ function addSymbol(bucket: SymbolBucket, if (shapedTextOrientations.vertical) { numVerticalGlyphVertices += addTextVertices( - bucket, anchor, shapedTextOrientations.vertical, layer, textAlongLine, feature, - textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, glyphPositionMap, verticalPlacedIconSymbolIndex, sizes); + bucket, anchor, shapedTextOrientations.vertical, imageMap, layer, textAlongLine, feature, + textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, verticalPlacedIconSymbolIndex, sizes); } const textBoxStartIndex = textCollisionFeature ? textCollisionFeature.boxStartIndex : bucket.collisionBoxArray.length; diff --git a/src/symbol/symbol_size.js b/src/symbol/symbol_size.js index 42a6355ad00..dab8f4b8989 100644 --- a/src/symbol/symbol_size.js +++ b/src/symbol/symbol_size.js @@ -8,7 +8,7 @@ import EvaluationParameters from '../style/evaluation_parameters'; import type {PropertyValue, PossiblyEvaluatedPropertyValue} from '../style/properties'; import type {InterpolationType} from '../style-spec/expression/definitions/interpolate'; -const SIZE_PACK_FACTOR = 256; +const SIZE_PACK_FACTOR = 128; export {getSizeData, evaluateSizeForFeature, evaluateSizeForZoom, SIZE_PACK_FACTOR}; diff --git a/test/expected/text-shaping-default.json b/test/expected/text-shaping-default.json index bf75ee0d5bb..c46e29fb6c1 100644 --- a/test/expected/text-shaping-default.json +++ b/test/expected/text-shaping-default.json @@ -1,49 +1,124 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -32.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -19.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde", @@ -52,6 +127,6 @@ "left": -32.5, "right": 32.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 1 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-images-horizontal.json b/test/expected/text-shaping-images-horizontal.json new file mode 100644 index 00000000000..b0178b19f2b --- /dev/null +++ b/test/expected/text-shaping-images-horizontal.json @@ -0,0 +1,275 @@ +{ + "positionedLines": [ + { + "positionedGlyphs": [ + { + "glyph": 70, + "imageName": null, + "x": -53, + "y": -34.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 18, + "left": 2, + "top": -8, + "advance": 12 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 111, + "imageName": null, + "x": -41, + "y": -34.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 13, + "height": 15, + "left": 1, + "top": -12, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 111, + "imageName": null, + "x": -27, + "y": -34.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 13, + "height": 15, + "left": 1, + "top": -12, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 57344, + "imageName": "square", + "x": -13, + "y": -31.5, + "vertical": false, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 1, + "metrics": { + "width": 14, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 16 + } + }, + { + "glyph": 57345, + "imageName": "wide", + "x": 8, + "y": -31.5, + "vertical": false, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 2, + "metrics": { + "width": 30, + "height": 14, + "left": 1, + "top": -3, + "advance": 30 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 16 + } + } + ], + "lineOffset": 0 + }, + { + "positionedGlyphs": [ + { + "glyph": 57346, + "imageName": "tall", + "x": -42, + "y": -10.5, + "vertical": false, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 4, + "metrics": { + "width": 14, + "height": 30, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 32 + } + }, + { + "glyph": 57347, + "imageName": "square", + "x": -21, + "y": 13.5, + "vertical": false, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 5, + "metrics": { + "width": 14, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 16 + } + }, + { + "glyph": 32, + "imageName": null, + "x": 0, + "y": 10.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 0, + "height": 0, + "left": 0, + "top": -26, + "advance": 6 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": 6, + "y": 10.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 97, + "imageName": null, + "x": 20, + "y": 10.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 114, + "imageName": null, + "x": 33, + "y": 10.5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 8, + "height": 14, + "left": 2, + "top": -12, + "advance": 9 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 21 + } + ], + "text": "Foo\n bar", + "top": -34.5, + "bottom": 34.5, + "left": -53, + "right": 53, + "writingMode": 1, + "iconsInText": true, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-images-vertical.json b/test/expected/text-shaping-images-vertical.json new file mode 100644 index 00000000000..1aef4d6fe64 --- /dev/null +++ b/test/expected/text-shaping-images-vertical.json @@ -0,0 +1,160 @@ +{ + "positionedLines": [ + { + "positionedGlyphs": [ + { + "glyph": 19977, + "imageName": null, + "x": -33, + "y": -13.5, + "vertical": true, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 57344, + "imageName": "square", + "x": -9, + "y": -10.5, + "vertical": true, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 1, + "metrics": { + "width": 14, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 16 + } + }, + { + "glyph": 57345, + "imageName": "wide", + "x": 12, + "y": -10.5, + "vertical": true, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 2, + "metrics": { + "width": 30, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 16 + } + } + ], + "lineOffset": 21 + }, + { + "positionedGlyphs": [ + { + "glyph": 57346, + "imageName": "tall", + "x": -43.5, + "y": -10.5, + "vertical": true, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 4, + "metrics": { + "width": 14, + "height": 30, + "left": 1, + "top": -3, + "advance": 30 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 32 + } + }, + { + "glyph": 57347, + "imageName": "square", + "x": 1.5, + "y": 13.5, + "vertical": true, + "scale": 1.5, + "fontStack": "", + "sectionIndex": 5, + "metrics": { + "width": 14, + "height": 14, + "left": 1, + "top": -3, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 16, + "h": 16 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 22.5, + "y": 10.5, + "vertical": true, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 6, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 + } + ], + "text": "三​三", + "top": -34.5, + "bottom": 34.5, + "left": -45, + "right": 45, + "writingMode": 2, + "iconsInText": true, + "verticalizable": true +} \ No newline at end of file diff --git a/test/expected/text-shaping-linebreak.json b/test/expected/text-shaping-linebreak.json index f0f9b4430b4..c0e69fa3c66 100644 --- a/test/expected/text-shaping-linebreak.json +++ b/test/expected/text-shaping-linebreak.json @@ -1,94 +1,244 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -32.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 98, - "x": -19.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 97, - "x": -32.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -19.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde abcde", @@ -97,6 +247,6 @@ "left": -32.5, "right": 32.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 2 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-newline.json b/test/expected/text-shaping-newline.json index a02ed316308..2eb5df9bba8 100644 --- a/test/expected/text-shaping-newline.json +++ b/test/expected/text-shaping-newline.json @@ -1,94 +1,244 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -32.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -29, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 98, - "x": -19.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -29, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 97, - "x": -32.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -19.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -5, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -5, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde\nabcde", @@ -97,6 +247,6 @@ "left": -32.5, "right": 32.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 2 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-newlines-in-middle.json b/test/expected/text-shaping-newlines-in-middle.json index 27f1ea28f35..dfcfd638756 100644 --- a/test/expected/text-shaping-newlines-in-middle.json +++ b/test/expected/text-shaping-newlines-in-middle.json @@ -1,94 +1,248 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -32.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 98, - "x": -19.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [], + "lineOffset": 0 }, { - "glyph": 99, - "x": -5.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 97, - "x": -32.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -19.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 5.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 19.5, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -32.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -19.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 5.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 19.5, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde\n\nabcde", @@ -97,6 +251,6 @@ "left": -32.5, "right": 32.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 3 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-null.json b/test/expected/text-shaping-null.json index febc8a01e0b..52a63e9835c 100644 --- a/test/expected/text-shaping-null.json +++ b/test/expected/text-shaping-null.json @@ -1,22 +1,55 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 104, - "x": -10, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 105, - "x": 4, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 104, + "imageName": null, + "x": -10, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 19, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 105, + "imageName": null, + "x": 4, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 4, + "height": 18, + "left": 1, + "top": -8, + "advance": 6 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "hi\u0000", @@ -25,6 +58,6 @@ "left": -10, "right": 10, "writingMode": 1, - "yOffset": -17, - "lineCount": 1 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-spacing.json b/test/expected/text-shaping-spacing.json index 960771413aa..53161f0c8b7 100644 --- a/test/expected/text-shaping-spacing.json +++ b/test/expected/text-shaping-spacing.json @@ -1,49 +1,124 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 97, - "x": -38.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 98, - "x": -22.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 99, - "x": -5.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 100, - "x": 8.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 101, - "x": 25.5, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 97, + "imageName": null, + "x": -38.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 11, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 98, + "imageName": null, + "x": -22.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 2, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 99, + "imageName": null, + "x": -5.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 10, + "height": 15, + "left": 1, + "top": -12, + "advance": 11 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 100, + "imageName": null, + "x": 8.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 20, + "left": 1, + "top": -7, + "advance": 14 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 101, + "imageName": null, + "x": 25.5, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 12, + "height": 15, + "left": 1, + "top": -12, + "advance": 13 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "abcde", @@ -52,6 +127,6 @@ "left": -38.5, "right": 38.5, "writingMode": 1, - "yOffset": -17, - "lineCount": 1 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/expected/text-shaping-zero-width-space.json b/test/expected/text-shaping-zero-width-space.json index 85959cc53db..6fc1c2a38a2 100644 --- a/test/expected/text-shaping-zero-width-space.json +++ b/test/expected/text-shaping-zero-width-space.json @@ -1,130 +1,341 @@ { - "positionedGlyphs": [ + "positionedLines": [ { - "glyph": 19977, - "x": -63, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 19977, + "imageName": null, + "x": -63, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": -42, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": -21, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 0, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 21, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 42, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 19977, - "x": -42, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 19977, + "imageName": null, + "x": -63, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": -42, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": -21, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 0, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 21, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 42, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 }, { - "glyph": 19977, - "x": -21, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 0, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 21, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 42, - "y": -41, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": -63, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": -42, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": -21, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 0, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 21, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 42, - "y": -17, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": -21, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 - }, - { - "glyph": 19977, - "x": 0, - "y": 7, - "vertical": false, - "scale": 1, - "fontStack": "Test", - "sectionIndex": 0 + "positionedGlyphs": [ + { + "glyph": 19977, + "imageName": null, + "x": -21, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + }, + { + "glyph": 19977, + "imageName": null, + "x": 0, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test", + "sectionIndex": 0, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + }, + "rect": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ], + "lineOffset": 0 } ], "text": "三三​三三​三三​三三三三三三​三三", @@ -133,6 +344,6 @@ "left": -63, "right": 63, "writingMode": 1, - "yOffset": -17, - "lineCount": 3 -} + "iconsInText": false, + "verticalizable": false +} \ No newline at end of file diff --git a/test/fixtures/fontstack-glyphs.json b/test/fixtures/fontstack-glyphs.json index 7dbbcee7e49..bb8c7967f28 100644 --- a/test/fixtures/fontstack-glyphs.json +++ b/test/fixtures/fontstack-glyphs.json @@ -7,7 +7,8 @@ "left": 0, "top": -26, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "33": { "id": 33, @@ -17,7 +18,8 @@ "left": 1, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "34": { "id": 34, @@ -27,7 +29,8 @@ "left": 1, "top": -8, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "35": { "id": 35, @@ -37,7 +40,8 @@ "left": 0, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "36": { "id": 36, @@ -47,7 +51,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "37": { "id": 37, @@ -57,7 +62,8 @@ "left": 1, "top": -8, "advance": 19 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "38": { "id": 38, @@ -67,7 +73,8 @@ "left": 1, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "39": { "id": 39, @@ -77,7 +84,8 @@ "left": 1, "top": -8, "advance": 5 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "40": { "id": 40, @@ -87,7 +95,8 @@ "left": 0, "top": -8, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "41": { "id": 41, @@ -97,7 +106,8 @@ "left": 0, "top": -8, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "42": { "id": 42, @@ -107,7 +117,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "43": { "id": 43, @@ -117,7 +128,8 @@ "left": 1, "top": -11, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "44": { "id": 44, @@ -127,7 +139,8 @@ "left": 0, "top": -23, "advance": 5 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "45": { "id": 45, @@ -137,7 +150,8 @@ "left": 0, "top": -18, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "46": { "id": 46, @@ -147,7 +161,8 @@ "left": 1, "top": -23, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "47": { "id": 47, @@ -157,7 +172,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "48": { "id": 48, @@ -167,7 +183,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "49": { "id": 49, @@ -177,7 +194,8 @@ "left": 2, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "50": { "id": 50, @@ -187,7 +205,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "51": { "id": 51, @@ -197,7 +216,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "52": { "id": 52, @@ -207,7 +227,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "53": { "id": 53, @@ -217,7 +238,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "54": { "id": 54, @@ -227,7 +249,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "55": { "id": 55, @@ -237,7 +260,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "56": { "id": 56, @@ -247,7 +271,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "57": { "id": 57, @@ -257,7 +282,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "58": { "id": 58, @@ -267,7 +293,8 @@ "left": 1, "top": -12, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "59": { "id": 59, @@ -277,7 +304,8 @@ "left": 0, "top": -12, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "60": { "id": 60, @@ -287,7 +315,8 @@ "left": 1, "top": -11, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "61": { "id": 61, @@ -297,7 +326,8 @@ "left": 1, "top": -14, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "62": { "id": 62, @@ -307,7 +337,8 @@ "left": 1, "top": -11, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "63": { "id": 63, @@ -317,7 +348,8 @@ "left": 0, "top": -8, "advance": 10 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "64": { "id": 64, @@ -327,7 +359,8 @@ "left": 1, "top": -8, "advance": 21 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "65": { "id": 65, @@ -337,7 +370,8 @@ "left": 0, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "66": { "id": 66, @@ -347,7 +381,8 @@ "left": 2, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "67": { "id": 67, @@ -357,7 +392,8 @@ "left": 1, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "68": { "id": 68, @@ -367,7 +403,8 @@ "left": 2, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "69": { "id": 69, @@ -377,7 +414,8 @@ "left": 2, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "70": { "id": 70, @@ -387,7 +425,8 @@ "left": 2, "top": -8, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "71": { "id": 71, @@ -397,7 +436,8 @@ "left": 1, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "72": { "id": 72, @@ -407,7 +447,8 @@ "left": 2, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "73": { "id": 73, @@ -417,7 +458,8 @@ "left": 2, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "74": { "id": 74, @@ -427,7 +469,8 @@ "left": -2, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "75": { "id": 75, @@ -437,7 +480,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "76": { "id": 76, @@ -447,7 +491,8 @@ "left": 2, "top": -8, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "77": { "id": 77, @@ -457,7 +502,8 @@ "left": 2, "top": -8, "advance": 21 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "78": { "id": 78, @@ -467,7 +513,8 @@ "left": 2, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "79": { "id": 79, @@ -477,7 +524,8 @@ "left": 1, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "80": { "id": 80, @@ -487,7 +535,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "81": { "id": 81, @@ -497,7 +546,8 @@ "left": 1, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "82": { "id": 82, @@ -507,7 +557,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "83": { "id": 83, @@ -517,7 +568,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "84": { "id": 84, @@ -527,7 +579,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "85": { "id": 85, @@ -537,7 +590,8 @@ "left": 2, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "86": { "id": 86, @@ -547,7 +601,8 @@ "left": 0, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "87": { "id": 87, @@ -557,7 +612,8 @@ "left": 0, "top": -8, "advance": 22 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "88": { "id": 88, @@ -567,7 +623,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "89": { "id": 89, @@ -577,7 +634,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "90": { "id": 90, @@ -587,7 +645,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "91": { "id": 91, @@ -597,7 +656,8 @@ "left": 1, "top": -8, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "92": { "id": 92, @@ -607,7 +667,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "93": { "id": 93, @@ -617,7 +678,8 @@ "left": 0, "top": -8, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "94": { "id": 94, @@ -627,7 +689,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "95": { "id": 95, @@ -637,7 +700,8 @@ "left": -1, "top": -28, "advance": 10 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "96": { "id": 96, @@ -647,7 +711,8 @@ "left": 4, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "97": { "id": 97, @@ -657,7 +722,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "98": { "id": 98, @@ -667,7 +733,8 @@ "left": 2, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "99": { "id": 99, @@ -677,7 +744,8 @@ "left": 1, "top": -12, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "100": { "id": 100, @@ -687,7 +755,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "101": { "id": 101, @@ -697,7 +766,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "102": { "id": 102, @@ -707,7 +777,8 @@ "left": 0, "top": -7, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "103": { "id": 103, @@ -717,7 +788,8 @@ "left": 0, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "104": { "id": 104, @@ -727,7 +799,8 @@ "left": 2, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "105": { "id": 105, @@ -737,7 +810,8 @@ "left": 1, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "106": { "id": 106, @@ -747,7 +821,8 @@ "left": -2, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "107": { "id": 107, @@ -757,7 +832,8 @@ "left": 2, "top": -7, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "108": { "id": 108, @@ -767,7 +843,8 @@ "left": 2, "top": -7, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "109": { "id": 109, @@ -777,7 +854,8 @@ "left": 2, "top": -12, "advance": 22 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "110": { "id": 110, @@ -787,7 +865,8 @@ "left": 2, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "111": { "id": 111, @@ -797,7 +876,8 @@ "left": 1, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "112": { "id": 112, @@ -807,7 +887,8 @@ "left": 2, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "113": { "id": 113, @@ -817,7 +898,8 @@ "left": 1, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "114": { "id": 114, @@ -827,7 +909,8 @@ "left": 2, "top": -12, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "115": { "id": 115, @@ -837,7 +920,8 @@ "left": 1, "top": -12, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "116": { "id": 116, @@ -847,7 +931,8 @@ "left": 0, "top": -10, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "117": { "id": 117, @@ -857,7 +942,8 @@ "left": 1, "top": -13, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "118": { "id": 118, @@ -867,7 +953,8 @@ "left": 0, "top": -13, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "119": { "id": 119, @@ -877,7 +964,8 @@ "left": 0, "top": -13, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "120": { "id": 120, @@ -887,7 +975,8 @@ "left": 0, "top": -13, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "121": { "id": 121, @@ -897,7 +986,8 @@ "left": 0, "top": -13, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "122": { "id": 122, @@ -907,7 +997,8 @@ "left": 0, "top": -13, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "123": { "id": 123, @@ -917,7 +1008,8 @@ "left": 0, "top": -8, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "124": { "id": 124, @@ -927,7 +1019,8 @@ "left": 5, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "125": { "id": 125, @@ -937,7 +1030,8 @@ "left": 0, "top": -8, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "126": { "id": 126, @@ -947,7 +1041,8 @@ "left": 1, "top": -16, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "160": { "id": 160, @@ -957,7 +1052,8 @@ "left": 0, "top": -26, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "161": { "id": 161, @@ -967,7 +1063,8 @@ "left": 1, "top": -12, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "162": { "id": 162, @@ -977,7 +1074,8 @@ "left": 2, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "163": { "id": 163, @@ -987,7 +1085,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "164": { "id": 164, @@ -997,7 +1096,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "165": { "id": 165, @@ -1007,7 +1107,8 @@ "left": 0, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "166": { "id": 166, @@ -1017,7 +1118,8 @@ "left": 5, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "167": { "id": 167, @@ -1027,7 +1129,8 @@ "left": 1, "top": -7, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "168": { "id": 168, @@ -1037,7 +1140,8 @@ "left": 3, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "169": { "id": 169, @@ -1047,7 +1151,8 @@ "left": 1, "top": -8, "advance": 19 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "170": { "id": 170, @@ -1057,7 +1162,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "171": { "id": 171, @@ -1067,7 +1173,8 @@ "left": 0, "top": -14, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "172": { "id": 172, @@ -1077,7 +1184,8 @@ "left": 1, "top": -16, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "173": { "id": 173, @@ -1087,7 +1195,8 @@ "left": 0, "top": -18, "advance": 7 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "174": { "id": 174, @@ -1097,7 +1206,8 @@ "left": 1, "top": -8, "advance": 19 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "175": { "id": 175, @@ -1107,7 +1217,8 @@ "left": -1, "top": -6, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "176": { "id": 176, @@ -1117,7 +1228,8 @@ "left": 1, "top": -8, "advance": 10 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "177": { "id": 177, @@ -1127,7 +1239,8 @@ "left": 1, "top": -11, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "178": { "id": 178, @@ -1137,7 +1250,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "179": { "id": 179, @@ -1147,7 +1261,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "180": { "id": 180, @@ -1157,7 +1272,8 @@ "left": 4, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "181": { "id": 181, @@ -1167,7 +1283,8 @@ "left": 2, "top": -13, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "182": { "id": 182, @@ -1177,7 +1294,8 @@ "left": 1, "top": -7, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "183": { "id": 183, @@ -1187,7 +1305,8 @@ "left": 1, "top": -15, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "184": { "id": 184, @@ -1197,7 +1316,8 @@ "left": 0, "top": -26, "advance": 5 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "185": { "id": 185, @@ -1207,7 +1327,8 @@ "left": 0, "top": -8, "advance": 8 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "186": { "id": 186, @@ -1217,7 +1338,8 @@ "left": 0, "top": -8, "advance": 9 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "187": { "id": 187, @@ -1227,7 +1349,8 @@ "left": 0, "top": -14, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "188": { "id": 188, @@ -1237,7 +1360,8 @@ "left": 0, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "189": { "id": 189, @@ -1247,7 +1371,8 @@ "left": 0, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "190": { "id": 190, @@ -1257,7 +1382,8 @@ "left": 0, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "191": { "id": 191, @@ -1267,7 +1393,8 @@ "left": 0, "top": -12, "advance": 10 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "192": { "id": 192, @@ -1277,7 +1404,8 @@ "left": 0, "top": -3, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "193": { "id": 193, @@ -1287,7 +1415,8 @@ "left": 0, "top": -3, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "194": { "id": 194, @@ -1297,7 +1426,8 @@ "left": 0, "top": -3, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "195": { "id": 195, @@ -1307,7 +1437,8 @@ "left": 0, "top": -4, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "196": { "id": 196, @@ -1317,7 +1448,8 @@ "left": 0, "top": -4, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "197": { "id": 197, @@ -1327,7 +1459,8 @@ "left": 0, "top": -4, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "198": { "id": 198, @@ -1337,7 +1470,8 @@ "left": -1, "top": -8, "advance": 20 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "199": { "id": 199, @@ -1347,7 +1481,8 @@ "left": 1, "top": -8, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "200": { "id": 200, @@ -1357,7 +1492,8 @@ "left": 2, "top": -3, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "201": { "id": 201, @@ -1367,7 +1503,8 @@ "left": 2, "top": -3, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "202": { "id": 202, @@ -1377,7 +1514,8 @@ "left": 2, "top": -3, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "203": { "id": 203, @@ -1387,7 +1525,8 @@ "left": 2, "top": -4, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "204": { "id": 204, @@ -1397,7 +1536,8 @@ "left": -1, "top": -3, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "205": { "id": 205, @@ -1407,7 +1547,8 @@ "left": 1, "top": -3, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "206": { "id": 206, @@ -1417,7 +1558,8 @@ "left": -1, "top": -3, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "207": { "id": 207, @@ -1427,7 +1569,8 @@ "left": -1, "top": -4, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "208": { "id": 208, @@ -1437,7 +1580,8 @@ "left": 0, "top": -8, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "209": { "id": 209, @@ -1447,7 +1591,8 @@ "left": 2, "top": -4, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "210": { "id": 210, @@ -1457,7 +1602,8 @@ "left": 1, "top": -3, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "211": { "id": 211, @@ -1467,7 +1613,8 @@ "left": 1, "top": -3, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "212": { "id": 212, @@ -1477,7 +1624,8 @@ "left": 1, "top": -3, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "213": { "id": 213, @@ -1487,7 +1635,8 @@ "left": 1, "top": -4, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "214": { "id": 214, @@ -1497,7 +1646,8 @@ "left": 1, "top": -4, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "215": { "id": 215, @@ -1507,7 +1657,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "216": { "id": 216, @@ -1517,7 +1668,8 @@ "left": 1, "top": -8, "advance": 18 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "217": { "id": 217, @@ -1527,7 +1679,8 @@ "left": 2, "top": -3, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "218": { "id": 218, @@ -1537,7 +1690,8 @@ "left": 2, "top": -3, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "219": { "id": 219, @@ -1547,7 +1701,8 @@ "left": 2, "top": -3, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "220": { "id": 220, @@ -1557,7 +1712,8 @@ "left": 2, "top": -4, "advance": 17 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "221": { "id": 221, @@ -1567,7 +1723,8 @@ "left": 0, "top": -3, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "222": { "id": 222, @@ -1577,7 +1734,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "223": { "id": 223, @@ -1587,7 +1745,8 @@ "left": 2, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "224": { "id": 224, @@ -1597,7 +1756,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "225": { "id": 225, @@ -1607,7 +1767,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "226": { "id": 226, @@ -1617,7 +1778,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "227": { "id": 227, @@ -1627,7 +1789,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "228": { "id": 228, @@ -1637,7 +1800,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "229": { "id": 229, @@ -1647,7 +1811,8 @@ "left": 1, "top": -6, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "230": { "id": 230, @@ -1657,7 +1822,8 @@ "left": 1, "top": -12, "advance": 20 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "231": { "id": 231, @@ -1667,7 +1833,8 @@ "left": 1, "top": -12, "advance": 11 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "232": { "id": 232, @@ -1677,7 +1844,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "233": { "id": 233, @@ -1687,7 +1855,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "234": { "id": 234, @@ -1697,7 +1866,8 @@ "left": 1, "top": -7, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "235": { "id": 235, @@ -1707,7 +1877,8 @@ "left": 1, "top": -8, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "236": { "id": 236, @@ -1717,7 +1888,8 @@ "left": -1, "top": -7, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "237": { "id": 237, @@ -1727,7 +1899,8 @@ "left": 1, "top": -7, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "238": { "id": 238, @@ -1737,7 +1910,8 @@ "left": -1, "top": -7, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "239": { "id": 239, @@ -1747,7 +1921,8 @@ "left": -1, "top": -8, "advance": 6 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "240": { "id": 240, @@ -1757,7 +1932,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "241": { "id": 241, @@ -1767,7 +1943,8 @@ "left": 2, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "242": { "id": 242, @@ -1777,7 +1954,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "243": { "id": 243, @@ -1787,7 +1965,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "244": { "id": 244, @@ -1797,7 +1976,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "245": { "id": 245, @@ -1807,7 +1987,8 @@ "left": 1, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "246": { "id": 246, @@ -1817,7 +1998,8 @@ "left": 1, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "247": { "id": 247, @@ -1827,7 +2009,8 @@ "left": 1, "top": -12, "advance": 13 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "248": { "id": 248, @@ -1837,7 +2020,8 @@ "left": 1, "top": -12, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "249": { "id": 249, @@ -1847,7 +2031,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "250": { "id": 250, @@ -1857,7 +2042,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "251": { "id": 251, @@ -1867,7 +2053,8 @@ "left": 1, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "252": { "id": 252, @@ -1877,7 +2064,8 @@ "left": 1, "top": -8, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "253": { "id": 253, @@ -1887,7 +2075,8 @@ "left": 0, "top": -7, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "254": { "id": 254, @@ -1897,7 +2086,8 @@ "left": 2, "top": -7, "advance": 14 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "255": { "id": 255, @@ -1907,7 +2097,8 @@ "left": 0, "top": -8, "advance": 12 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "256": { "id": 256, @@ -1917,7 +2108,8 @@ "left": 0, "top": -5, "advance": 15 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} }, "19977": { "id": 19977, @@ -1927,6 +2119,7 @@ "left": 2, "top": -8, "advance": 21 - } + }, + "rect": {"x": 0, "y": 0, "w": 32, "h": 32} } } diff --git a/test/integration/expression-tests/format/basic/test.json b/test/integration/expression-tests/format/basic/test.json index d4a9a32010b..3f6eeaddc55 100644 --- a/test/integration/expression-tests/format/basic/test.json +++ b/test/integration/expression-tests/format/basic/test.json @@ -40,24 +40,28 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null }, { "text": "b", + "image": null, "scale": 2, "fontStack": null, "textColor": null }, { "text": "c", + "image": null, "scale": null, "fontStack": "a,b", "textColor": null }, { "text": "d", + "image": null, "scale": null, "fontStack": null, "textColor": {"r":0,"g":1,"b":0,"a":1} diff --git a/test/integration/expression-tests/format/coercion/test.json b/test/integration/expression-tests/format/coercion/test.json index 60980b55672..213eb961575 100644 --- a/test/integration/expression-tests/format/coercion/test.json +++ b/test/integration/expression-tests/format/coercion/test.json @@ -21,18 +21,21 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null }, { "text": "1", + "image": null, "scale": null, "fontStack": null, "textColor": null }, { "text": "true", + "image": null, "scale": null, "fontStack": null, "textColor": null diff --git a/test/integration/expression-tests/format/data-driven-font/test.json b/test/integration/expression-tests/format/data-driven-font/test.json index dc0793d3169..c81fff8e8f1 100644 --- a/test/integration/expression-tests/format/data-driven-font/test.json +++ b/test/integration/expression-tests/format/data-driven-font/test.json @@ -22,6 +22,7 @@ "sections": [ { "text": "a", + "image": null, "scale": 1.5, "fontStack": null, "textColor": null @@ -32,6 +33,7 @@ "sections": [ { "text": "a", + "image": null, "scale": 0.5, "fontStack": null, "textColor": null diff --git a/test/integration/expression-tests/format/image-sections/test.json b/test/integration/expression-tests/format/image-sections/test.json new file mode 100644 index 00000000000..63f40ec477c --- /dev/null +++ b/test/integration/expression-tests/format/image-sections/test.json @@ -0,0 +1,54 @@ +{ + "expression": [ + "format", + ["image", "monument-15"], + ["image", "beach-11"] + ], + "inputs": [ + [ + {"availableImages": ["monument-15"]}, + {} + ] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": true, + "isZoomConstant": true, + "type": "formatted" + }, + "outputs": [ + { + "sections": [ + { + "text": "", + "image": {"name": "monument-15", "available": true}, + "scale": null, + "fontStack": null, + "textColor": null + }, + { + "text": "", + "image": {"name": "beach-11", "available": false}, + "scale": null, + "fontStack": null, + "textColor": null + } + ] + } + ], + "serialized": [ + "format", + [ + "image", + "monument-15" + ], + {}, + [ + "image", + "beach-11" + ], + {} + ] + } +} diff --git a/test/integration/expression-tests/format/implicit-coerce/test.json b/test/integration/expression-tests/format/implicit-coerce/test.json index 24533043cdd..6b0877bc6a9 100644 --- a/test/integration/expression-tests/format/implicit-coerce/test.json +++ b/test/integration/expression-tests/format/implicit-coerce/test.json @@ -20,6 +20,7 @@ "sections": [ { "text": "", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -30,6 +31,7 @@ "sections": [ { "text": "0", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -40,6 +42,7 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null diff --git a/test/integration/expression-tests/format/implicit-omit/test.json b/test/integration/expression-tests/format/implicit-omit/test.json index 01d9f9e9d6f..c586958ede4 100644 --- a/test/integration/expression-tests/format/implicit-omit/test.json +++ b/test/integration/expression-tests/format/implicit-omit/test.json @@ -20,6 +20,7 @@ "sections": [ { "text": "", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -30,6 +31,7 @@ "sections": [ { "text": "0", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -40,6 +42,7 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null diff --git a/test/integration/expression-tests/format/implicit/test.json b/test/integration/expression-tests/format/implicit/test.json index f9e2a151cce..097bdf814e4 100644 --- a/test/integration/expression-tests/format/implicit/test.json +++ b/test/integration/expression-tests/format/implicit/test.json @@ -20,6 +20,7 @@ "sections": [ { "text": "", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -30,6 +31,7 @@ "sections": [ { "text": "0", + "image": null, "scale": null, "fontStack": null, "textColor": null @@ -40,6 +42,7 @@ "sections": [ { "text": "a", + "image": null, "scale": null, "fontStack": null, "textColor": null diff --git a/test/integration/render-tests/text-field/formatted-arabic/expected.png b/test/integration/render-tests/text-field/formatted-arabic/expected.png index fa904bda075..cbb7afa2252 100644 Binary files a/test/integration/render-tests/text-field/formatted-arabic/expected.png and b/test/integration/render-tests/text-field/formatted-arabic/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-images-constant-size/expected.png b/test/integration/render-tests/text-field/formatted-images-constant-size/expected.png new file mode 100644 index 00000000000..cc27f0bcdce Binary files /dev/null and b/test/integration/render-tests/text-field/formatted-images-constant-size/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-images-constant-size/style.json b/test/integration/render-tests/text-field/formatted-images-constant-size/style.json new file mode 100644 index 00000000000..64be0e32c3d --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images-constant-size/style.json @@ -0,0 +1,59 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 20 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald@2x", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "point", + "layout": { + "text-offset": [0.1, 0], + "text-field": ["format", "Square ", ["image", "default_1"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + }, + { + "id": "images", + "type": "symbol", + "source": "point", + "layout": { + "text-offset": [0, 1.2], + "text-size": 30, + "text-field": ["format", "Square ", ["image", "default_1"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-images-line/expected.png b/test/integration/render-tests/text-field/formatted-images-line/expected.png new file mode 100644 index 00000000000..5fefdc0490e Binary files /dev/null and b/test/integration/render-tests/text-field/formatted-images-line/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-images-line/style.json b/test/integration/render-tests/text-field/formatted-images-line/style.json new file mode 100644 index 00000000000..2b5ad09a887 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images-line/style.json @@ -0,0 +1,82 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 160, + "width": 100 + } + }, + "center": [ -0.0055, 0.0065 ], + "zoom": 14, + "bearing": 60, + "sources": { + "line": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "geometry": { + "coordinates": [ + [ + -0.008778156849501784, + 0.004124676107977621 + ], + [ + -0.00581685092268458, + 0.007085982020228698 + ], + [ + -0.0037016324048408933, + 0.0072975038696938554 + ], + [ + -0.0007403264791605579, + 0.006239894618133235 + ], + [ + 0.0009518483364558961, + 0.004336197959602828 + ] + ], + "type": "LineString" + }, + "type": "Feature", + "properties": {} + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "line", + "type": "symbol", + "source": "line", + "layout": { + "symbol-placement": "line", + "symbol-spacing": 100, + "text-field": ["format", "London", + ["image", "london-overground"], + ["image", "london-underground"], + ["image", "national-rail"], + ["image", "dlr"] + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + }, + { + "id": "outline", + "type": "line", + "source": "line", + "paint": { + "line-opacity": 0.2 + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-images-multiline/expected.png b/test/integration/render-tests/text-field/formatted-images-multiline/expected.png new file mode 100644 index 00000000000..1d2c7701d60 Binary files /dev/null and b/test/integration/render-tests/text-field/formatted-images-multiline/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-images-multiline/style.json b/test/integration/render-tests/text-field/formatted-images-multiline/style.json new file mode 100644 index 00000000000..7f71170e9b0 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images-multiline/style.json @@ -0,0 +1,75 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "multiline": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 30 ] + } + } + ] + } + }, + "multiline_with_scale": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, -18 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "multiline", + "type": "symbol", + "source": "multiline", + "layout": { + "text-max-width": 4, + "text-field": ["format", "London", ["image", "london-underground.national-rail"], + "Berlin", ["image", "s-bahn.u-bahn"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + }, + { + "id": "multiline_with_scale", + "type": "symbol", + "source": "multiline_with_scale", + "layout": { + "text-max-width": 8, + "text-field": ["format", "Paris", ["image", "rer.transilien"], + "USA", ["image", "interstate_1"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/expected.png b/test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/expected.png new file mode 100644 index 00000000000..18696d91b95 Binary files /dev/null and b/test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/style.json b/test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/style.json new file mode 100644 index 00000000000..9db78cb894c --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images-variable-anchors-justification/style.json @@ -0,0 +1,103 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 160, + "width": 200 + } + }, + "center": [ 0.1, 0 ], + "zoom": 0, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald@2x", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "point", + "layout": { + "text-justify": "auto", + "text-radial-offset": 2.55, + "text-variable-anchor": ["center", "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"], + "text-field": ["format", "Square", "\n", ["image", "default_1"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-images-vertical/expected.png b/test/integration/render-tests/text-field/formatted-images-vertical/expected.png new file mode 100644 index 00000000000..db1901df9e2 Binary files /dev/null and b/test/integration/render-tests/text-field/formatted-images-vertical/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-images-vertical/style.json b/test/integration/render-tests/text-field/formatted-images-vertical/style.json new file mode 100644 index 00000000000..7b8e34555a9 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images-vertical/style.json @@ -0,0 +1,46 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 168, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "vertical": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "vertical", + "type": "symbol", + "source": "vertical", + "layout": { + "text-writing-mode": ["vertical"], + "text-field": ["format", "London", ["image", "london-overground.london-underground.national-rail"], + "IH", ["image", "interstate_1"], ["image", "government_icon"], "ッ",{"font-scale": 1.8}], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/expected.png b/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/expected.png new file mode 100644 index 00000000000..69caad5f216 Binary files /dev/null and b/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/style.json b/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/style.json new file mode 100644 index 00000000000..343c4e05fa9 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images-zoom-dependent-size/style.json @@ -0,0 +1,69 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128, + "allowed": 0.014 + } + }, + "center": [ 0, 0 ], + "zoom": 12, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald@2x", + "layers": [ + { + "id": "text_zoom_constant", + "type": "symbol", + "source": "point", + "layout": { + "text-offset": [0, -1], + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 11, + 8, + 13, + 34 + ], + "text-field": ["format", "Zoom", ["image", "rer"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + }, + { + "id": "text_zoom_dependent", + "type": "symbol", + "source": "point", + "layout": { + "text-offset": [0, 1], + "text-size": 21, + "text-field": ["format", "Zoom", ["image", "rer"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-images/expected.png b/test/integration/render-tests/text-field/formatted-images/expected.png new file mode 100644 index 00000000000..dd6707b6877 Binary files /dev/null and b/test/integration/render-tests/text-field/formatted-images/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-images/style.json b/test/integration/render-tests/text-field/formatted-images/style.json new file mode 100644 index 00000000000..bcae63e6548 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-images/style.json @@ -0,0 +1,64 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ 0, 20 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sprite": "local://sprites/emerald@2x", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "point", + "layout": { + "icon-image": ["image", "london-overground"], + "icon-offset": [0, -20], + "text-field": ["format", "London", ["image", "london-underground"]], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + }, + { + "id": "images", + "type": "symbol", + "source": "point", + "layout": { + "text-offset": [0, 2], + "text-size": 20, + "text-field": ["format", ["image", "london-overground"], + ["image", "london-underground"], + ["image", "dlr"], + ["image", "u-bahn"] + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ] + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted-text-color/expected.png b/test/integration/render-tests/text-field/formatted-text-color/expected.png index 41c8bde1166..03a96d89cde 100644 Binary files a/test/integration/render-tests/text-field/formatted-text-color/expected.png and b/test/integration/render-tests/text-field/formatted-text-color/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted/expected.png b/test/integration/render-tests/text-field/formatted/expected.png index 08f856cb509..c120b5a0662 100644 Binary files a/test/integration/render-tests/text-field/formatted/expected.png and b/test/integration/render-tests/text-field/formatted/expected.png differ diff --git a/test/unit/style-spec/fixture/text-field-format.input.json b/test/unit/style-spec/fixture/text-field-format.input.json index f78ab7754a5..7ac15d2e10a 100644 --- a/test/unit/style-spec/fixture/text-field-format.input.json +++ b/test/unit/style-spec/fixture/text-field-format.input.json @@ -24,7 +24,7 @@ "glyphs": "local://glyphs/{fontstack}/{range}.pbf", "layers": [ { - "id": "no options - invalid", + "id": "no options - valid", "type": "symbol", "source": "point", "layout": { @@ -34,6 +34,27 @@ ] } }, + { + "id": "empty options - invalid", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + {} + ] + } + }, + { + "id": "no parameters - invalid", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format" + ] + } + }, { "id": "empty options - valid", "type": "symbol", @@ -156,6 +177,47 @@ } ] } + }, + { + "id": "image section with empty options - valid", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "image-name"], + {} + ] + } + }, + { + "id": "image section with no options - valid", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "image-name"] + ] + } + }, + { + "id": "mixed sections with options - valid", + "type": "symbol", + "source": "point", + "layout": { + "text-field": [ + "format", + ["image", "image-name"], + "some text", + { + "text-font": ["literal", ["Helvetica"]], + "font-scale": 1.5 + }, + "\n", + ["image", "another-image"] + ] + } } ] } diff --git a/test/unit/style-spec/fixture/text-field-format.output-api-supported.json b/test/unit/style-spec/fixture/text-field-format.output-api-supported.json index 5776ae616fd..a5b090bb751 100644 --- a/test/unit/style-spec/fixture/text-field-format.output-api-supported.json +++ b/test/unit/style-spec/fixture/text-field-format.output-api-supported.json @@ -1,19 +1,23 @@ [ { - "message": "layers[0].layout.text-field: Expected at least two arguments.", - "line": 31 + "message": "layers[1].layout.text-field: First argument must be an image or text section.", + "line": 42 }, { - "message": "layers[4].layout.text-field[1]: Expected number but found string instead.", - "line": 78 + "message": "layers[2].layout.text-field: Expected at least one argument.", + "line": 53 }, { - "message": "layers[5].layout.text-field[1][0]: Unknown expression \"Helvetica\". If you wanted a literal array, use [\"literal\", [...]].", - "line": 90 + "message": "layers[6].layout.text-field[1]: Expected number but found string instead.", + "line": 99 }, { - "message": "layers[8].layout.text-field[1]: Expected array but found number instead.", - "line": 126 + "message": "layers[7].layout.text-field[1][0]: Unknown expression \"Helvetica\". If you wanted a literal array, use [\"literal\", [...]].", + "line": 111 + }, + { + "message": "layers[10].layout.text-field[1]: Expected array but found number instead.", + "line": 147 }, { "message": "glyphs: Styles must reference glyphs hosted by Mapbox", diff --git a/test/unit/style-spec/fixture/text-field-format.output.json b/test/unit/style-spec/fixture/text-field-format.output.json index c8bdaf47bfe..a8423368e43 100644 --- a/test/unit/style-spec/fixture/text-field-format.output.json +++ b/test/unit/style-spec/fixture/text-field-format.output.json @@ -1,18 +1,22 @@ [ { - "message": "layers[0].layout.text-field: Expected at least two arguments.", - "line": 31 + "message": "layers[1].layout.text-field: First argument must be an image or text section.", + "line": 42 }, { - "message": "layers[4].layout.text-field[1]: Expected number but found string instead.", - "line": 78 + "message": "layers[2].layout.text-field: Expected at least one argument.", + "line": 53 }, { - "message": "layers[5].layout.text-field[1][0]: Unknown expression \"Helvetica\". If you wanted a literal array, use [\"literal\", [...]].", - "line": 90 + "message": "layers[6].layout.text-field[1]: Expected number but found string instead.", + "line": 99 }, { - "message": "layers[8].layout.text-field[1]: Expected array but found number instead.", - "line": 126 + "message": "layers[7].layout.text-field[1][0]: Unknown expression \"Helvetica\". If you wanted a literal array, use [\"literal\", [...]].", + "line": 111 + }, + { + "message": "layers[10].layout.text-field[1]: Expected array but found number instead.", + "line": 147 } ] \ No newline at end of file diff --git a/test/unit/symbol/quads.test.js b/test/unit/symbol/quads.test.js index 68dea30b067..f97a7959fd6 100644 --- a/test/unit/symbol/quads.test.js +++ b/test/unit/symbol/quads.test.js @@ -15,7 +15,7 @@ test('getIconQuads', (t) => { bottom: 5.5, left: -7.5, image - }, 0), [{ + }, 0, true), [{ tl: {x: -8.5, y: -6.5}, tr: {x: 8.5, y: -6.5}, bl: {x: -8.5, y: 6.5}, @@ -23,6 +23,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: true, sectionIndex: 0 }], 'icon-anchor: center'); @@ -32,7 +33,7 @@ test('getIconQuads', (t) => { bottom: 11, left: -15, image - }, 0), [{ + }, 0, false), [{ tl: {x: -17, y: -13}, tr: {x: 17, y: -13}, bl: {x: -17, y: 13}, @@ -40,6 +41,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: false, sectionIndex: 0 }], 'icon-anchor: center icon, icon-scale: 2'); @@ -49,7 +51,7 @@ test('getIconQuads', (t) => { bottom: 11, left: -15, image - }, 0), [{ + }, 0, false), [{ tl: {x: -16, y: -1}, tr: {x: 1, y: -1}, bl: {x: -16, y: 12}, @@ -57,6 +59,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: false, sectionIndex: 0 }], 'icon-anchor: top-right'); @@ -66,7 +69,7 @@ test('getIconQuads', (t) => { bottom: 5.5, left: -30, image - }, 0), [{ + }, 0, false), [{ tl: {x: -34, y: -6.5}, tr: {x: 34, y: -6.5}, bl: {x: -34, y: 6.5}, @@ -74,6 +77,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: false, sectionIndex: 0 }], 'icon-text-fit: both'); @@ -87,7 +91,7 @@ test('getIconQuads', (t) => { bottom: 5.5, left: -7.5, image - }, 0), [{ + }, 0, false), [{ tl: {x: -8.5, y: -6.5}, tr: {x: 8.5, y: -6.5}, bl: {x: -8.5, y: 6.5}, @@ -95,6 +99,7 @@ test('getIconQuads', (t) => { tex: {x: 0, y: 0, w: 17, h: 13}, writingMode: null, glyphOffset: [0, 0], + isSDF: false, sectionIndex: 0 }]); t.end(); diff --git a/test/unit/symbol/shaping.test.js b/test/unit/symbol/shaping.test.js index b4dd8ebed10..33278229af2 100644 --- a/test/unit/symbol/shaping.test.js +++ b/test/unit/symbol/shaping.test.js @@ -2,7 +2,8 @@ import {test} from '../../util/test'; import fs from 'fs'; import path from 'path'; import * as shaping from '../../../src/symbol/shaping'; -import Formatted from '../../../src/style-spec/expression/types/formatted'; +import Formatted, {FormattedSection} from '../../../src/style-spec/expression/types/formatted'; +import ResolvedImage from '../../../src/style-spec/expression/types/resolved_image'; import {ImagePosition} from '../../../src/render/image_atlas'; const WritingMode = shaping.WritingMode; @@ -13,46 +14,63 @@ if (typeof process !== 'undefined' && process.env !== undefined) { test('shaping', (t) => { const oneEm = 24; + const layoutTextSize = 16; + const layoutTextSizeThisZoom = 16; const fontStack = 'Test'; const glyphs = { 'Test': JSON.parse(fs.readFileSync(path.join(__dirname, '/../../fixtures/fontstack-glyphs.json'))) }; + const glyphPositions = glyphs; + + const images = { + 'square': new ImagePosition({x: 0, y: 0, w: 16, h: 16}, {pixelRatio: 1, version: 1}), + 'tall': new ImagePosition({x: 0, y: 0, w: 16, h: 32}, {pixelRatio: 1, version: 1}), + 'wide': new ImagePosition({x: 0, y: 0, w: 32, h: 16}, {pixelRatio: 1, version: 1}), + }; + + const sectionForImage = (name) => { + return new FormattedSection('', ResolvedImage.fromString(name), null, null, null); + }; + + const sectionForText = (name, scale) => { + return new FormattedSection(name, null, scale, null, null); + }; let shaped; JSON.parse('{}'); - shaped = shaping.shapeText(Formatted.fromString(`hi${String.fromCharCode(0)}`), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString(`hi${String.fromCharCode(0)}`), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-null.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-null.json')))); // Default shaping. - shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-default.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-default.json')))); // Letter spacing. - shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0.125 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0.125 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-spacing.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-spacing.json')))); // Line break. - shaped = shaping.shapeText(Formatted.fromString('abcde abcde'), glyphs, fontStack, 4 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde abcde'), glyphs, glyphPositions, images, fontStack, 4 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-linebreak.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, require('../../expected/text-shaping-linebreak.json')); const expectedNewLine = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-newline.json'))); - shaped = shaping.shapeText(Formatted.fromString('abcde\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde\nabcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-newline.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedNewLine); - shaped = shaping.shapeText(Formatted.fromString('abcde\r\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point'); - t.deepEqual(shaped.positionedGlyphs, expectedNewLine.positionedGlyphs); + shaped = shaping.shapeText(Formatted.fromString('abcde\r\nabcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + t.deepEqual(shaped.positionedLines, expectedNewLine.positionedLines); const expectedNewLinesInMiddle = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-newlines-in-middle.json'))); - shaped = shaping.shapeText(Formatted.fromString('abcde\n\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('abcde\n\nabcde'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-newlines-in-middle.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedNewLinesInMiddle); @@ -60,21 +78,62 @@ test('shaping', (t) => { // a position is ideal for breaking. const expectedZeroWidthSpaceBreak = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-zero-width-space.json'))); - shaped = shaping.shapeText(Formatted.fromString('三三\u200b三三\u200b三三\u200b三三三三三三\u200b三三'), glyphs, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString('三三\u200b三三\u200b三三\u200b三三三三三三\u200b三三'), glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-zero-width-space.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedZeroWidthSpaceBreak); // Null shaping. - shaped = shaping.shapeText(Formatted.fromString(''), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString(''), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); t.equal(false, shaped); - shaped = shaping.shapeText(Formatted.fromString(String.fromCharCode(0)), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); + shaped = shaping.shapeText(Formatted.fromString(String.fromCharCode(0)), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); t.equal(false, shaped); // https://github.com/mapbox/mapbox-gl-js/issues/3254 - shaped = shaping.shapeText(Formatted.fromString(' foo bar\n'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); - const shaped2 = shaping.shapeText(Formatted.fromString('foo bar'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point'); - t.same(shaped.positionedGlyphs, shaped2.positionedGlyphs); + shaped = shaping.shapeText(Formatted.fromString(' foo bar\n'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + const shaped2 = shaping.shapeText(Formatted.fromString('foo bar'), glyphs, glyphPositions, images, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + t.same(shaped.positionedLines, shaped2.positionedLines); + + t.test('basic image shaping', (t) => { + const shaped = shaping.shapeText(new Formatted([sectionForImage('square')]), glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + t.same(shaped.top, -12); // 1em line height + t.same(shaped.left, -10.5); // 16 - 2px border * 1.5 scale factor + t.end(); + }); + + t.test('images in horizontal layout', (t) => { + const expectedImagesHorizontal = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-images-horizontal.json'))); + const horizontalFormatted = new Formatted([ + sectionForText('Foo'), + sectionForImage('square'), + sectionForImage('wide'), + sectionForText('\n'), + sectionForImage('tall'), + sectionForImage('square'), + sectionForText(' bar'), + ]); + const shaped = shaping.shapeText(horizontalFormatted, glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal, false, 'point', layoutTextSize, layoutTextSizeThisZoom); + if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-images-horizontal.json'), JSON.stringify(shaped, null, 2)); + t.deepEqual(shaped, expectedImagesHorizontal); + t.end(); + }); + + t.test('images in vertical layout', (t) => { + const expectedImagesVertical = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-images-vertical.json'))); + const horizontalFormatted = new Formatted([ + sectionForText('三'), + sectionForImage('square'), + sectionForImage('wide'), + sectionForText('\u200b'), + sectionForImage('tall'), + sectionForImage('square'), + sectionForText('三'), + ]); + const shaped = shaping.shapeText(horizontalFormatted, glyphs, glyphPositions, images, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.vertical, true, 'point', layoutTextSize, layoutTextSizeThisZoom); + if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-images-vertical.json'), JSON.stringify(shaped, null, 2)); + t.deepEqual(shaped, expectedImagesVertical); + t.end(); + }); t.end(); });