diff --git a/src/data/array_group.js b/src/data/array_group.js index 87ffd7868f3..d2124c9ac4c 100644 --- a/src/data/array_group.js +++ b/src/data/array_group.js @@ -19,6 +19,8 @@ class Segment { * A group has: * * * A "layout" vertex array, with fixed attributes, containing values calculated from layout properties. + * * Zero or one dynamic "layout" vertex arrays, with fixed attributes containing values that can be + * * recalculated each frame on the cpu. * * Zero, one, or two element arrays, with fixed layout, for eventual `gl.drawElements` use. * * Zero or more "paint" vertex arrays keyed by layer ID, each with a dynamic layout which depends * on which paint properties of that layer use data-driven-functions (property functions or @@ -38,6 +40,11 @@ class ArrayGroup { const LayoutVertexArrayType = createVertexArrayType(programInterface.layoutAttributes); this.layoutVertexArray = new LayoutVertexArrayType(); + if (programInterface.dynamicLayoutAttributes) { + const DynamicLayoutVertexArrayType = createVertexArrayType(programInterface.dynamicLayoutAttributes); + this.dynamicLayoutVertexArray = new DynamicLayoutVertexArrayType(); + } + const ElementArrayType = programInterface.elementArrayType; if (ElementArrayType) this.elementArray = new ElementArrayType(); @@ -99,6 +106,7 @@ class ArrayGroup { serialize(transferables) { return { layoutVertexArray: this.layoutVertexArray.serialize(transferables), + dynamicLayoutVertexArray: this.dynamicLayoutVertexArray && this.dynamicLayoutVertexArray.serialize(transferables), elementArray: this.elementArray && this.elementArray.serialize(transferables), elementArray2: this.elementArray2 && this.elementArray2.serialize(transferables), paintVertexArrays: serializePaintVertexArrays(this.layerData, transferables), diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 9a15cba5f73..3cd3dc6336f 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -21,6 +21,7 @@ const CollisionFeature = require('../../symbol/collision_feature'); const findPoleOfInaccessibility = require('../../util/find_pole_of_inaccessibility'); const classifyRings = require('../../util/classify_rings'); const VectorTileFeature = require('vector-tile').VectorTileFeature; +const createStructArrayType = require('../../util/struct_array'); const shapeText = Shaping.shapeText; const shapeIcon = Shaping.shapeIcon; @@ -28,17 +29,51 @@ const WritingMode = Shaping.WritingMode; const getGlyphQuads = Quads.getGlyphQuads; const getIconQuads = Quads.getIconQuads; +const PlacedSymbolArray = createStructArrayType({ + members: [ + { type: 'Int16', name: 'anchorX' }, + { type: 'Int16', name: 'anchorY' }, + { type: 'Uint16', name: 'glyphStartIndex' }, + { type: 'Uint16', name: 'numGlyphs' }, + { type: 'Uint32', name: 'lineStartIndex' }, + { type: 'Uint32', name: 'lineLength' }, + { type: 'Uint16', name: 'segment' }, + { type: 'Uint16', name: 'lowerSize' }, + { type: 'Uint16', name: 'upperSize' }, + { type: 'Float32', name: 'lineOffsetX' }, + { type: 'Float32', name: 'lineOffsetY' }, + { type: 'Float32', name: 'placementZoom' }, + { type: 'Uint8', name: 'vertical' } + ] +}); + +const GlyphOffsetArray = createStructArrayType({ + members: [ + { type: 'Float32', name: 'offsetX' } + ] +}); + +const LineVertexArray = createStructArrayType({ + members: [ + { type: 'Int16', name: 'x' }, + { type: 'Int16', name: 'y' } + ]}); + const elementArrayType = createElementArrayType(); const layoutAttributes = [ {name: 'a_pos_offset', components: 4, type: 'Int16'}, - {name: 'a_label_pos', components: 2, type: 'Int16'}, {name: 'a_data', components: 4, type: 'Uint16'} ]; +const dynamicLayoutAttributes = [ + { name: 'a_projected_pos', components: 3, type: 'Float32' } +]; + const symbolInterfaces = { glyph: { layoutAttributes: layoutAttributes, + dynamicLayoutAttributes: dynamicLayoutAttributes, elementArrayType: elementArrayType, paintAttributes: [ {name: 'a_fill_color', property: 'text-color', type: 'Uint8'}, @@ -50,6 +85,7 @@ const symbolInterfaces = { }, icon: { layoutAttributes: layoutAttributes, + dynamicLayoutAttributes: dynamicLayoutAttributes, elementArrayType: elementArrayType, paintAttributes: [ {name: 'a_fill_color', property: 'icon-color', type: 'Uint8'}, @@ -70,34 +106,26 @@ const symbolInterfaces = { } }; -function addVertex(array, x, y, ox, oy, labelX, labelY, tx, ty, sizeVertex, minzoom, maxzoom, labelminzoom, labelangle) { +function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex, labelminzoom) { array.emplaceBack( // a_pos_offset - x, - y, + anchorX, + anchorY, Math.round(ox * 64), Math.round(oy * 64), - // a_label_pos - labelX, - labelY, - // a_data tx, // x coordinate of symbol on glyph atlas texture ty, // y coordinate of symbol on glyph atlas texture packUint8ToFloat( (labelminzoom || 0) * 10, // labelminzoom - labelangle % 256 // labelangle - ), - packUint8ToFloat( - (minzoom || 0) * 10, // minzoom - Math.min(maxzoom || 25, 25) * 10 // maxzoom + 0 // unused 8 bits ), + 0, // unused 16 bits // a_size sizeVertex ? sizeVertex[0] : undefined, - sizeVertex ? sizeVertex[1] : undefined, - sizeVertex ? sizeVertex[2] : undefined + sizeVertex ? sizeVertex[1] : undefined ); } @@ -188,6 +216,12 @@ class SymbolBucket { } this.textSizeData = options.textSizeData; this.iconSizeData = options.iconSizeData; + + this.placedGlyphArray = new PlacedSymbolArray(options.placedGlyphArray); + this.placedIconArray = new PlacedSymbolArray(options.placedIconArray); + this.glyphOffsetArray = new GlyphOffsetArray(options.glyphOffsetArray); + this.lineVertexArray = new LineVertexArray(options.lineVertexArray); + } else { this.textSizeData = getSizeData(this.zoom, layer, 'text-size'); this.iconSizeData = getSizeData(this.zoom, layer, 'icon-size'); @@ -294,6 +328,10 @@ class SymbolBucket { textSizeData: this.textSizeData, iconSizeData: this.iconSizeData, fontstack: this.fontstack, + placedGlyphArray: this.placedGlyphArray.serialize(transferables), + placedIconArray: this.placedIconArray.serialize(transferables), + glyphOffsetArray: this.glyphOffsetArray.serialize(transferables), + lineVertexArray: this.lineVertexArray.serialize(transferables), arrays: util.mapObject(this.arrays, (a) => a.isEmpty() ? null : a.serialize(transferables)) }; } @@ -416,6 +454,9 @@ class SymbolBucket { const layoutTextSize = this.layers[0].getLayoutValue('text-size', {zoom: this.zoom + 1}, feature.properties); const layoutIconSize = this.layers[0].getLayoutValue('icon-size', {zoom: this.zoom + 1}, feature.properties); + const textOffset = this.layers[0].getLayoutValue('text-offset', {zoom: this.zoom }, feature.properties); + const iconOffset = this.layers[0].getLayoutValue('icon-offset', {zoom: this.zoom }, feature.properties); + // 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. // This calculates text-size at a high zoom level so that all tiles can @@ -460,8 +501,8 @@ class SymbolBucket { const addToBuffers = inside || mayOverlap; this.addSymbolInstance(anchor, line, shapedTextOrientations, shapedIcon, this.layers[0], addToBuffers, this.collisionBoxArray, feature.index, feature.sourceLayerIndex, this.index, - textBoxScale, textPadding, textAlongLine, - iconBoxScale, iconPadding, iconAlongLine, + textBoxScale, textPadding, textAlongLine, textOffset, + iconBoxScale, iconPadding, iconAlongLine, iconOffset, {zoom: this.zoom}, feature.properties); }; @@ -529,6 +570,11 @@ class SymbolBucket { this.createArrays(); + this.placedGlyphArray = new PlacedSymbolArray(); + this.placedIconArray = new PlacedSymbolArray(); + this.glyphOffsetArray = new GlyphOffsetArray(); + this.lineVertexArray = new LineVertexArray(); + const layer = this.layers[0]; const layout = layer.layout; @@ -603,6 +649,14 @@ class SymbolBucket { // Insert final placement into collision tree and add glyphs/icons to buffers + if (!hasText && !hasIcon) continue; + const line = symbolInstance.line; + const lineStartIndex = this.lineVertexArray.length; + for (let i = 0; i < line.length; i++) { + this.lineVertexArray.emplaceBack(line[i].x, line[i].y); + } + const lineLength = this.lineVertexArray.length - lineStartIndex; + if (hasText) { collisionTile.insertCollisionFeature(textCollisionFeature, glyphScale, layout['text-ignore-placement']); @@ -618,11 +672,15 @@ class SymbolBucket { glyphScale, textSizeData, layout['text-keep-upright'], + symbolInstance.textOffset, textAlongLine, collisionTile.angle, symbolInstance.featureProperties, symbolInstance.writingModes, - symbolInstance.anchor); + symbolInstance.anchor, + lineStartIndex, + lineLength, + this.placedGlyphArray); } } @@ -641,11 +699,15 @@ class SymbolBucket { iconScale, iconSizeData, layout['icon-keep-upright'], + symbolInstance.iconOffset, iconAlongLine, collisionTile.angle, symbolInstance.featureProperties, null, - symbolInstance.anchor + symbolInstance.anchor, + lineStartIndex, + lineLength, + this.placedIconArray ); } } @@ -655,55 +717,63 @@ class SymbolBucket { if (showCollisionBoxes) this.addToDebugBuffers(collisionTile); } - addSymbols(arrays, quads, scale, sizeVertex, keepUpright, alongLine, placementAngle, featureProperties, writingModes, labelAnchor) { + addSymbols(arrays, quads, scale, sizeVertex, keepUpright, lineOffset, alongLine, placementAngle, featureProperties, writingModes, labelAnchor, lineStartIndex, lineLength, placedSymbolArray) { const elementArray = arrays.elementArray; const layoutVertexArray = arrays.layoutVertexArray; + const dynamicLayoutVertexArray = arrays.dynamicLayoutVertexArray; const zoom = this.zoom; const placementZoom = Math.max(Math.log(scale) / Math.LN2 + zoom, 0); + const glyphOffsetArrayStart = this.glyphOffsetArray.length; + + const labelAngle = Math.abs((labelAnchor.angle + placementAngle) % Math.PI); + const inVerticalRange = labelAngle > Math.PI / 4 && labelAngle <= Math.PI * 3 / 4; + const useVerticalMode = Boolean(writingModes & WritingMode.vertical) && inVerticalRange; + for (const symbol of quads) { - // drop incorrectly oriented glyphs - const a = (symbol.anchorAngle + placementAngle + Math.PI) % (Math.PI * 2); - if (writingModes & WritingMode.vertical) { - if (alongLine && symbol.writingMode === WritingMode.vertical) { - if (keepUpright && alongLine && a <= (Math.PI * 5 / 4) || a > (Math.PI * 7 / 4)) continue; - } else if (keepUpright && alongLine && a <= (Math.PI * 3 / 4) || a > (Math.PI * 5 / 4)) continue; - } else if (keepUpright && alongLine && (a <= Math.PI / 2 || a > Math.PI * 3 / 2)) continue; + + if (alongLine && keepUpright) { + // drop incorrectly oriented glyphs + if ((symbol.writingMode === WritingMode.vertical) !== useVerticalMode) continue; + } const tl = symbol.tl, tr = symbol.tr, bl = symbol.bl, br = symbol.br, - tex = symbol.tex, - anchorPoint = symbol.anchorPoint; - - let minZoom = Math.max(zoom + Math.log(symbol.minScale) / Math.LN2, placementZoom); - const maxZoom = Math.min(zoom + Math.log(symbol.maxScale) / Math.LN2, 25); - - if (maxZoom <= minZoom) continue; - - // Lower min zoom so that while fading out the label it can be shown outside of collision-free zoom levels - if (minZoom === placementZoom) minZoom = 0; - - // Encode angle of glyph - const glyphAngle = Math.round((symbol.glyphAngle / (Math.PI * 2)) * 256); + tex = symbol.tex; const segment = arrays.prepareSegment(4); const index = segment.vertexLength; - addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, tl.x, tl.y, labelAnchor.x, labelAnchor.y, tex.x, tex.y, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); - addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, tr.x, tr.y, labelAnchor.x, labelAnchor.y, tex.x + tex.w, tex.y, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); - addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, bl.x, bl.y, labelAnchor.x, labelAnchor.y, tex.x, tex.y + tex.h, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); - addVertex(layoutVertexArray, anchorPoint.x, anchorPoint.y, br.x, br.y, labelAnchor.x, labelAnchor.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, minZoom, maxZoom, placementZoom, glyphAngle); + const y = symbol.glyphOffset[1]; + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex, placementZoom); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex, placementZoom); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex, placementZoom); + addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, placementZoom); + + dynamicLayoutVertexArray.emplaceBack(labelAnchor.x, labelAnchor.y, 0); + dynamicLayoutVertexArray.emplaceBack(labelAnchor.x, labelAnchor.y, 0); + dynamicLayoutVertexArray.emplaceBack(labelAnchor.x, labelAnchor.y, 0); + dynamicLayoutVertexArray.emplaceBack(labelAnchor.x, labelAnchor.y, 0); elementArray.emplaceBack(index, index + 1, index + 2); elementArray.emplaceBack(index + 1, index + 2, index + 3); segment.vertexLength += 4; segment.primitiveLength += 2; + + this.glyphOffsetArray.emplaceBack(symbol.glyphOffset[0]); } + placedSymbolArray.emplaceBack(labelAnchor.x, labelAnchor.y, + glyphOffsetArrayStart, this.glyphOffsetArray.length - glyphOffsetArrayStart, + lineStartIndex, lineLength, labelAnchor.segment, + sizeVertex ? sizeVertex[0] : 0, sizeVertex ? sizeVertex[1] : 0, + lineOffset[0], lineOffset[1], + placementZoom, useVerticalMode); + arrays.populatePaintArrays(featureProperties); } @@ -777,8 +847,8 @@ class SymbolBucket { * @private */ addSymbolInstance(anchor, line, shapedTextOrientations, shapedIcon, layer, addToBuffers, collisionBoxArray, featureIndex, sourceLayerIndex, bucketIndex, - textBoxScale, textPadding, textAlongLine, - iconBoxScale, iconPadding, iconAlongLine, globalProperties, featureProperties) { + textBoxScale, textPadding, textAlongLine, textOffset, + iconBoxScale, iconPadding, iconAlongLine, iconOffset, globalProperties, featureProperties) { let textCollisionFeature, iconCollisionFeature; let iconQuads = []; @@ -788,7 +858,7 @@ class SymbolBucket { if (!shapedTextOrientations[writingMode]) continue; glyphQuads = glyphQuads.concat(addToBuffers ? getGlyphQuads(anchor, shapedTextOrientations[writingMode], - textBoxScale, line, layer, textAlongLine, + layer, textAlongLine, globalProperties, featureProperties) : []); textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedTextOrientations[writingMode], textBoxScale, textPadding, textAlongLine, false); @@ -799,7 +869,7 @@ class SymbolBucket { if (shapedIcon) { iconQuads = addToBuffers ? - getIconQuads(anchor, shapedIcon, iconBoxScale, line, layer, + getIconQuads(anchor, shapedIcon, layer, iconAlongLine, shapedTextOrientations[WritingMode.horizontal], globalProperties, featureProperties) : []; @@ -824,7 +894,10 @@ class SymbolBucket { iconBoxEndIndex, glyphQuads, iconQuads, + textOffset, + iconOffset, anchor, + line, featureIndex, featureProperties, writingModes @@ -894,10 +967,9 @@ function getSizeAttributeDeclarations(layer, sizeProperty) { ) { // composite function: // [ text-size(lowerZoomStop, feature), - // text-size(upperZoomStop, feature), - // layoutSize == text-size(layoutZoomLevel, feature) ] + // text-size(upperZoomStop, feature)] return [{ - name: 'a_size', components: 3, type: 'Uint16' + name: 'a_size', components: 2, type: 'Uint16' }]; } // constant or camera function @@ -920,8 +992,7 @@ function getSizeVertexData(layer, tileZoom, stopZoomLevels, sizeProperty, featur // composite function return [ 10 * layer.getLayoutValue(sizeProperty, {zoom: stopZoomLevels[0]}, featureProperties), - 10 * layer.getLayoutValue(sizeProperty, {zoom: stopZoomLevels[1]}, featureProperties), - 10 * layer.getLayoutValue(sizeProperty, {zoom: 1 + tileZoom}, featureProperties) + 10 * layer.getLayoutValue(sizeProperty, {zoom: stopZoomLevels[1]}, featureProperties) ]; } // camera function or constant diff --git a/src/data/buffer.js b/src/data/buffer.js index a5a29731507..4fa4e0fec22 100644 --- a/src/data/buffer.js +++ b/src/data/buffer.js @@ -9,7 +9,8 @@ const AttributeType = { Int8: 'BYTE', Uint8: 'UNSIGNED_BYTE', Int16: 'SHORT', - Uint16: 'UNSIGNED_SHORT' + Uint16: 'UNSIGNED_SHORT', + Float32: 'FLOAT' }; /** @@ -22,14 +23,16 @@ class Buffer { * @param {Object} array A serialized StructArray. * @param {Object} arrayType A serialized StructArrayType. * @param {BufferType} type + * @param {boolean} dynamicDraw Whether this buffer will be repeatedly updated. */ - constructor(array, arrayType, type) { + constructor(array, arrayType, type, dynamicDraw) { this.arrayBuffer = array.arrayBuffer; this.length = array.length; this.attributes = arrayType.members; this.itemSize = arrayType.bytesPerElement; this.type = type; this.arrayType = arrayType; + this.dynamicDraw = dynamicDraw; } static fromStructArray(array, type) { @@ -47,15 +50,27 @@ class Buffer { this.gl = gl; this.buffer = gl.createBuffer(); gl.bindBuffer(type, this.buffer); - gl.bufferData(type, this.arrayBuffer, gl.STATIC_DRAW); + gl.bufferData(type, this.arrayBuffer, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW); // dump array buffer once it's bound to gl this.arrayBuffer = null; } else { gl.bindBuffer(type, this.buffer); + + if (this.dynamicDraw && this.arrayBuffer) { + gl.bufferSubData(type, 0, this.arrayBuffer); + this.arrayBuffer = null; + } } } + /* + * @param {Object} array A serialized StructArray. + */ + updateData(array) { + this.arrayBuffer = array.arrayBuffer; + } + enableAttributes (gl, program) { for (let j = 0; j < this.attributes.length; j++) { const member = this.attributes[j]; diff --git a/src/data/buffer_group.js b/src/data/buffer_group.js index 11058afb366..52e28c0f228 100644 --- a/src/data/buffer_group.js +++ b/src/data/buffer_group.js @@ -18,6 +18,13 @@ class BufferGroup { this.layoutVertexBuffer = new Buffer(arrays.layoutVertexArray, LayoutVertexArrayType.serialize(), Buffer.BufferType.VERTEX); + if (arrays.dynamicLayoutVertexArray) { + const DynamicLayoutVertexArrayType = createVertexArrayType(programInterface.dynamicLayoutAttributes); + this.dynamicLayoutVertexArray = new DynamicLayoutVertexArrayType(arrays.dynamicLayoutVertexArray); + this.dynamicLayoutVertexBuffer = new Buffer(arrays.dynamicLayoutVertexArray, + DynamicLayoutVertexArrayType.serialize(), Buffer.BufferType.VERTEX, true); + } + if (arrays.elementArray) { this.elementBuffer = new Buffer(arrays.elementArray, programInterface.elementArrayType.serialize(), Buffer.BufferType.ELEMENT); @@ -49,6 +56,9 @@ class BufferGroup { destroy() { this.layoutVertexBuffer.destroy(); + if (this.dynamicLayoutVertexBuffer) { + this.dynamicLayoutVertexBuffer.destroy(); + } if (this.elementBuffer) { this.elementBuffer.destroy(); } diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index 7fd8f95abde..1088ee66fc8 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -1,10 +1,11 @@ 'use strict'; -const assert = require('assert'); -const util = require('../util/util'); const drawCollisionDebug = require('./draw_collision_debug'); const pixelsToTileUnits = require('../source/pixels_to_tile_units'); -const interpolationFactor = require('../style-spec/function').interpolationFactor; +const symbolProjection = require('../symbol/projection'); +const symbolSize = require('../symbol/symbol_size'); +const mat4 = require('@mapbox/gl-matrix').mat4; +const identityMat4 = mat4.identity(new Float32Array(16)); module.exports = drawSymbols; @@ -39,14 +40,16 @@ function drawSymbols(painter, sourceCache, layer, coords) { layer.layout['icon-rotation-alignment'], // icon-pitch-alignment is not yet implemented // and we simply inherit the rotation alignment - layer.layout['icon-rotation-alignment'] + layer.layout['icon-rotation-alignment'], + layer.layout['icon-keep-upright'] ); drawLayerSymbols(painter, sourceCache, layer, coords, true, layer.paint['text-translate'], layer.paint['text-translate-anchor'], layer.layout['text-rotation-alignment'], - layer.layout['text-pitch-alignment'] + layer.layout['text-pitch-alignment'], + layer.layout['text-keep-upright'] ); if (sourceCache.map.showCollisionBoxes) { @@ -55,7 +58,7 @@ function drawSymbols(painter, sourceCache, layer, coords) { } function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate, translateAnchor, - rotationAlignment, pitchAlignment) { + rotationAlignment, pitchAlignment, keepUpright) { if (!isText && painter.style.sprite && !painter.style.sprite.loaded()) return; @@ -64,6 +67,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate const rotateWithMap = rotationAlignment === 'map'; const pitchWithMap = pitchAlignment === 'map'; + const alongLine = rotateWithMap && layer.layout['symbol-placement'] === 'line'; const depthOn = pitchWithMap; @@ -97,13 +101,23 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate painter.enableTileClippingMask(coord); - gl.uniformMatrix4fv(program.u_matrix, false, - painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor)); + gl.uniformMatrix4fv(program.u_matrix, false, painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor)); + + const s = pixelsToTileUnits(tile, 1, painter.transform.zoom); + const labelPlaneMatrix = symbolProjection.getLabelPlaneMatrix(coord.posMatrix, pitchWithMap, rotateWithMap, painter.transform, s); + const glCoordMatrix = symbolProjection.getGlCoordMatrix(coord.posMatrix, pitchWithMap, rotateWithMap, painter.transform, s); + gl.uniformMatrix4fv(program.u_gl_coord_matrix, false, painter.translatePosMatrix(glCoordMatrix, tile, translate, translateAnchor, true)); + + if (alongLine) { + gl.uniformMatrix4fv(program.u_label_plane_matrix, false, identityMat4); + symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, keepUpright, s, layer); + } else { + gl.uniformMatrix4fv(program.u_label_plane_matrix, false, labelPlaneMatrix); + } gl.uniform1f(program.u_collision_y_stretch, tile.collisionTile.yStretch); - drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, - pitchWithMap); + drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, pitchWithMap); prevFontstack = bucket.fontstack; } @@ -116,7 +130,6 @@ function setSymbolDrawState(program, painter, layer, tileZoom, isText, isSDF, ro const gl = painter.gl; const tr = painter.transform; - gl.uniform1i(program.u_rotate_with_map, rotateWithMap); gl.uniform1i(program.u_pitch_with_map, pitchWithMap); gl.activeTexture(gl.TEXTURE0); @@ -147,68 +160,16 @@ function setSymbolDrawState(program, painter, layer, tileZoom, isText, isSDF, ro painter.frameHistory.bind(gl); gl.uniform1i(program.u_fadetexture, 1); - gl.uniform1f(program.u_zoom, tr.zoom); - gl.uniform1f(program.u_pitch, tr.pitch / 360 * 2 * Math.PI); - gl.uniform1f(program.u_bearing, tr.bearing / 360 * 2 * Math.PI); - gl.uniform1f(program.u_aspect_ratio, tr.width / tr.height); gl.uniform1i(program.u_is_size_zoom_constant, sizeData.isZoomConstant ? 1 : 0); gl.uniform1i(program.u_is_size_feature_constant, sizeData.isFeatureConstant ? 1 : 0); - if (!sizeData.isZoomConstant && !sizeData.isFeatureConstant) { - // composite function - const t = interpolationFactor(tr.zoom, - sizeData.functionBase, - sizeData.coveringZoomRange[0], - sizeData.coveringZoomRange[1] - ); - gl.uniform1f(program.u_size_t, util.clamp(t, 0, 1)); - } else if (sizeData.isFeatureConstant && !sizeData.isZoomConstant) { - // camera function - let size; - if (sizeData.functionType === 'interval') { - size = layer.getLayoutValue(isText ? 'text-size' : 'icon-size', - {zoom: tr.zoom}); - } else { - assert(sizeData.functionType === 'exponential'); - // Even though we could get the exact value of the camera function - // at z = tr.zoom, we intentionally do not: instead, we interpolate - // between the camera function values at a pair of zoom stops covering - // [tileZoom, tileZoom + 1] in order to be consistent with this - // restriction on composite functions - const t = sizeData.functionType === 'interval' ? 0 : - interpolationFactor(tr.zoom, - sizeData.functionBase, - sizeData.coveringZoomRange[0], - sizeData.coveringZoomRange[1]); - - const lowerValue = sizeData.coveringStopValues[0]; - const upperValue = sizeData.coveringStopValues[1]; - size = lowerValue + (upperValue - lowerValue) * util.clamp(t, 0, 1); - } - - gl.uniform1f(program.u_size, size); - gl.uniform1f(program.u_layout_size, sizeData.layoutSize); - } else if (sizeData.isFeatureConstant && sizeData.isZoomConstant) { - gl.uniform1f(program.u_size, sizeData.layoutSize); - } gl.uniform1f(program.u_camera_to_center_distance, tr.cameraToCenterDistance); - if (layer.layout['symbol-placement'] === 'line' && - layer.layout['text-rotation-alignment'] === 'map' && - layer.layout['text-pitch-alignment'] === 'viewport' && - layer.layout['text-field']) { - // We hide line labels with viewport alignment as they move into the distance - // because the approximations we use for drawing their glyphs get progressively worse - // The "1.5" here means we start hiding them when the distance from the label - // to the camera is 50% greater than the distance from the center of the map - // to the camera. Depending on viewport properties, you might expect this to filter - // the top third of the screen at pitch 60, and do almost nothing at pitch 45 - gl.uniform1f(program.u_max_camera_distance, 1.5); - } else { - // "10" is effectively infinite at any pitch we support - gl.uniform1f(program.u_max_camera_distance, 10); - } + + const size = symbolSize.evaluateSizeForZoom(sizeData, tr, layer, isText); + if (size.uSizeT !== undefined) gl.uniform1f(program.u_size_t, size.uSizeT); + if (size.uSize !== undefined) gl.uniform1f(program.u_size, size.uSize); } function drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, pitchWithMap) { @@ -216,20 +177,10 @@ function drawTileSymbols(program, programConfiguration, painter, layer, tile, bu const gl = painter.gl; const tr = painter.transform; - if (pitchWithMap) { - const s = pixelsToTileUnits(tile, 1, tr.zoom); - gl.uniform2f(program.u_extrude_scale, s, s); - } else { - const s = tr.cameraToCenterDistance; - gl.uniform2f(program.u_extrude_scale, - tr.pixelsToGLUnits[0] * s, - tr.pixelsToGLUnits[1] * s); - } - if (isSDF) { const haloWidthProperty = `${isText ? 'text' : 'icon'}-halo-width`; const hasHalo = !layer.isPaintValueFeatureConstant(haloWidthProperty) || layer.paint[haloWidthProperty]; - const gammaScale = (pitchWithMap ? Math.cos(tr._pitch) : 1) * tr.cameraToCenterDistance; + const gammaScale = (pitchWithMap ? Math.cos(tr._pitch) * tr.cameraToCenterDistance : 1); gl.uniform1f(program.u_gamma_scale, gammaScale); if (hasHalo) { // Draw halo underneath the text. @@ -248,7 +199,7 @@ function drawSymbolElements(buffers, layer, gl, program) { const paintVertexBuffer = layerData && layerData.paintVertexBuffer; for (const segment of buffers.segments) { - segment.vaos[layer.id].bind(gl, program, buffers.layoutVertexBuffer, buffers.elementBuffer, paintVertexBuffer, segment.vertexOffset); + segment.vaos[layer.id].bind(gl, program, buffers.layoutVertexBuffer, buffers.elementBuffer, paintVertexBuffer, segment.vertexOffset, buffers.dynamicLayoutVertexBuffer); gl.drawElements(gl.TRIANGLES, segment.primitiveLength * 3, gl.UNSIGNED_SHORT, segment.primitiveOffset * 3 * 2); } } diff --git a/src/render/frame_history.js b/src/render/frame_history.js index e9501f31f21..b181ab25fd3 100644 --- a/src/render/frame_history.js +++ b/src/render/frame_history.js @@ -47,6 +47,10 @@ class FrameHistory { this.previousZoom = zoom; } + isVisible(zoom) { + return this.opacities[Math.floor(zoom * 10)] !== 0; + } + bind(gl) { if (!this.texture) { this.texture = gl.createTexture(); diff --git a/src/render/painter.js b/src/render/painter.js index 6736d46f0f8..237093e39c7 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -292,19 +292,37 @@ class Painter { this.gl.depthRange(nearDepth, farDepth); } - translatePosMatrix(matrix, tile, translate, anchor) { + /** + * Transform a matrix to incorporate the *-translate and *-translate-anchor properties into it. + * @param {Float32Array} matrix + * @param {Tile} tile + * @param {Array} translate + * @param {string} anchor + * @param {boolean} inViewportPixelUnitsUnits True when the units accepted by the matrix are in viewport pixels instead of tile units. + * + * @returns {Float32Array} matrix + */ + translatePosMatrix(matrix, tile, translate, translateAnchor, inViewportPixelUnitsUnits) { if (!translate[0] && !translate[1]) return matrix; - if (anchor === 'viewport') { - const sinA = Math.sin(-this.transform.angle); - const cosA = Math.cos(-this.transform.angle); + const angle = inViewportPixelUnitsUnits ? + (translateAnchor === 'map' ? this.transform.angle : 0) : + (translateAnchor === 'viewport' ? -this.transform.angle : 0); + + if (angle) { + const sinA = Math.sin(angle); + const cosA = Math.cos(angle); translate = [ translate[0] * cosA - translate[1] * sinA, translate[0] * sinA + translate[1] * cosA ]; } - const translation = [ + const translation = inViewportPixelUnitsUnits ? [ + translate[0], + translate[1], + 0 + ] : [ pixelsToTileUnits(tile, translate[0], this.transform.zoom), pixelsToTileUnits(tile, translate[1], this.transform.zoom), 0 diff --git a/src/render/vertex_array_object.js b/src/render/vertex_array_object.js index 80ab6a31cff..e6b22a943c3 100644 --- a/src/render/vertex_array_object.js +++ b/src/render/vertex_array_object.js @@ -12,7 +12,7 @@ class VertexArrayObject { this.vao = null; } - bind(gl, program, layoutVertexBuffer, elementBuffer, vertexBuffer2, vertexOffset) { + bind(gl, program, layoutVertexBuffer, elementBuffer, vertexBuffer2, vertexOffset, dynamicVertexBuffer) { if (gl.extVertexArrayObject === undefined) { gl.extVertexArrayObject = gl.getExtension("OES_vertex_array_object"); @@ -24,18 +24,24 @@ class VertexArrayObject { this.boundVertexBuffer !== layoutVertexBuffer || this.boundVertexBuffer2 !== vertexBuffer2 || this.boundElementBuffer !== elementBuffer || - this.boundVertexOffset !== vertexOffset + this.boundVertexOffset !== vertexOffset || + this.boundDynamicVertexBuffer !== dynamicVertexBuffer ); if (!gl.extVertexArrayObject || isFreshBindRequired) { - this.freshBind(gl, program, layoutVertexBuffer, elementBuffer, vertexBuffer2, vertexOffset); + this.freshBind(gl, program, layoutVertexBuffer, elementBuffer, vertexBuffer2, vertexOffset, dynamicVertexBuffer); this.gl = gl; } else { gl.extVertexArrayObject.bindVertexArrayOES(this.vao); + + if (dynamicVertexBuffer) { + // The buffer may have been updated. Rebind to upload data. + dynamicVertexBuffer.bind(gl); + } } } - freshBind(gl, program, layoutVertexBuffer, elementBuffer, vertexBuffer2, vertexOffset) { + freshBind(gl, program, layoutVertexBuffer, elementBuffer, vertexBuffer2, vertexOffset, dynamicVertexBuffer) { let numPrevAttributes; const numNextAttributes = program.numAttributes; @@ -51,6 +57,7 @@ class VertexArrayObject { this.boundVertexBuffer2 = vertexBuffer2; this.boundElementBuffer = elementBuffer; this.boundVertexOffset = vertexOffset; + this.boundDynamicVertexBuffer = dynamicVertexBuffer; } else { numPrevAttributes = gl.currentNumAttributes || 0; @@ -69,6 +76,9 @@ class VertexArrayObject { if (vertexBuffer2) { vertexBuffer2.enableAttributes(gl, program); } + if (dynamicVertexBuffer) { + dynamicVertexBuffer.enableAttributes(gl, program); + } layoutVertexBuffer.bind(gl); layoutVertexBuffer.setVertexAttribPointers(gl, program, vertexOffset); @@ -76,6 +86,10 @@ class VertexArrayObject { vertexBuffer2.bind(gl); vertexBuffer2.setVertexAttribPointers(gl, program, vertexOffset); } + if (dynamicVertexBuffer) { + dynamicVertexBuffer.bind(gl); + dynamicVertexBuffer.setVertexAttribPointers(gl, program, vertexOffset); + } if (elementBuffer) { elementBuffer.bind(gl); } diff --git a/src/shaders/symbol_icon.vertex.glsl b/src/shaders/symbol_icon.vertex.glsl index 2cc973c5613..b326f37fd04 100644 --- a/src/shaders/symbol_icon.vertex.glsl +++ b/src/shaders/symbol_icon.vertex.glsl @@ -1,27 +1,25 @@ attribute vec4 a_pos_offset; -attribute vec2 a_label_pos; attribute vec4 a_data; +attribute vec3 a_projected_pos; // icon-size data (see symbol_sdf.vertex.glsl for more) -attribute vec3 a_size; +attribute vec2 a_size; 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 highp float u_layout_size; // used when size is feature constant uniform highp float u_camera_to_center_distance; uniform highp float u_pitch; uniform highp float u_collision_y_stretch; #pragma mapbox: define lowp float opacity -// matrix is for the vertex position. uniform mat4 u_matrix; +uniform mat4 u_label_plane_matrix; +uniform mat4 u_gl_coord_matrix; uniform bool u_is_text; -uniform highp float u_zoom; -uniform bool u_rotate_with_map; -uniform vec2 u_extrude_scale; +uniform bool u_pitch_with_map; uniform vec2 u_texsize; @@ -35,60 +33,45 @@ void main() { vec2 a_offset = a_pos_offset.zw; vec2 a_tex = a_data.xy; - highp vec2 label_data = unpack_float(a_data[2]); - highp float a_labelminzoom = label_data[0]; - highp vec2 a_zoom = unpack_float(a_data[3]); - highp float a_minzoom = a_zoom[0]; - highp float a_maxzoom = a_zoom[1]; + mediump vec2 label_data = unpack_float(a_data[2]); + mediump float a_labelminzoom = label_data[0]; float size; - // In order to accommodate placing labels around corners in - // symbol-placement: line, each glyph in a label could have multiple - // "quad"s only one of which should be shown at a given zoom level. - // The min/max zoom assigned to each quad is based on the font size at - // the vector tile's zoom level, which might be different than at the - // currently rendered zoom level if text-size is zoom-dependent. - // Thus, we compensate for this difference by calculating an adjustment - // based on the scale of rendered text size relative to layout text size. - highp float layoutSize; if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { size = mix(a_size[0], a_size[1], u_size_t) / 10.0; - layoutSize = a_size[2] / 10.0; } else if (u_is_size_zoom_constant && !u_is_size_feature_constant) { size = a_size[0] / 10.0; - layoutSize = size; } else if (!u_is_size_zoom_constant && u_is_size_feature_constant) { size = u_size; - layoutSize = u_layout_size; } else { size = u_size; - layoutSize = u_size; } - float fontScale = u_is_text ? size / 24.0 : size; + vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1); + highp float camera_to_anchor_distance = projectedPoint.w; + 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 = 0.5 + 0.5 * clamp(distance_ratio, 0.1, 10.0); - highp float zoomAdjust = log2(size / layoutSize); - highp float adjustedZoom = (u_zoom - zoomAdjust) * 10.0; - // result: z = 0 if a_minzoom <= adjustedZoom < a_maxzoom, and 1 otherwise - highp float z = 2.0 - step(a_minzoom, adjustedZoom) - (1.0 - step(a_maxzoom, adjustedZoom)); + size *= perspective_ratio; - vec4 projectedPoint = u_matrix * vec4(a_label_pos, 0, 1); - highp float camera_to_anchor_distance = projectedPoint.w; - highp float perspective_ratio = 1.0 + 0.5*((camera_to_anchor_distance / u_camera_to_center_distance) - 1.0); + float fontScale = u_is_text ? size / 24.0 : size; - vec2 extrude = fontScale * u_extrude_scale * perspective_ratio * (a_offset / 64.0); - if (u_rotate_with_map) { - gl_Position = u_matrix * vec4(a_pos + extrude, 0, 1); - gl_Position.z += z * gl_Position.w; - } else { - gl_Position = u_matrix * vec4(a_pos, 0, 1) + vec4(extrude, 0, 0); - } + highp float segment_angle = -a_projected_pos[2]; + highp float angle_sin = sin(segment_angle); + highp float angle_cos = cos(segment_angle); + 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_gl_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 64.0 * fontScale), 0.0, 1.0); v_tex = a_tex / u_texsize; // See comments in symbol_sdf.vertex highp float incidence_stretch = camera_to_anchor_distance / (u_camera_to_center_distance * cos(u_pitch)); highp float collision_adjustment = max(1.0, incidence_stretch / u_collision_y_stretch); - highp float perspective_zoom_adjust = floor(log2(perspective_ratio * collision_adjustment) * 10.0); + highp float collision_perspective_ratio = 1.0 + 0.5*((camera_to_anchor_distance / u_camera_to_center_distance) - 1.0); + highp float perspective_zoom_adjust = floor(log2(collision_perspective_ratio * collision_adjustment) * 10.0); v_fade_tex = vec2((a_labelminzoom + perspective_zoom_adjust) / 255.0, 0.0); } diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl index 79a8092cf4d..42e4fd3bfc3 100644 --- a/src/shaders/symbol_sdf.vertex.glsl +++ b/src/shaders/symbol_sdf.vertex.glsl @@ -1,8 +1,8 @@ const float PI = 3.141592653589793; attribute vec4 a_pos_offset; -attribute vec2 a_label_pos; attribute vec4 a_data; +attribute vec3 a_projected_pos; // contents of a_size vary based on the type of property value // used for {text,icon}-size. @@ -10,14 +10,12 @@ attribute vec4 a_data; // 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), -// layoutSize == text-size(layoutZoomLevel, feature) ] -attribute vec3 a_size; +// text-size(upperZoomStop, feature) ] +attribute vec2 a_size; 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 highp float u_layout_size; // used when size is feature constant #pragma mapbox: define highp vec4 fill_color #pragma mapbox: define highp vec4 halo_color @@ -25,38 +23,21 @@ uniform highp float u_layout_size; // used when size is feature constant #pragma mapbox: define lowp float halo_width #pragma mapbox: define lowp float halo_blur -// matrix is for the vertex position. uniform mat4 u_matrix; +uniform mat4 u_label_plane_matrix; +uniform mat4 u_gl_coord_matrix; uniform bool u_is_text; -uniform highp float u_zoom; -uniform bool u_rotate_with_map; uniform bool u_pitch_with_map; uniform highp float u_pitch; -uniform highp float u_bearing; -uniform highp float u_aspect_ratio; uniform highp float u_camera_to_center_distance; -uniform highp float u_max_camera_distance; uniform highp float u_collision_y_stretch; -uniform vec2 u_extrude_scale; uniform vec2 u_texsize; varying vec4 v_data0; varying vec2 v_data1; -// Used below to move the vertex out of the clip space for when the current -// zoom is out of the glyph's zoom range. -highp float clipUnusedGlyphAngles(const highp float render_size, - const highp float layout_size, - const highp float min_zoom, - const highp float max_zoom) { - highp float zoom_adjust = log2(render_size / layout_size); - highp float adjusted_zoom = (u_zoom - zoom_adjust) * 10.0; - // result: 0 if min_zoom <= adjusted_zoom < max_zoom, and 1 otherwise - return 2.0 - step(min_zoom, adjusted_zoom) - (1.0 - step(max_zoom, adjusted_zoom)); -} - void main() { #pragma mapbox: initialize highp vec4 fill_color #pragma mapbox: initialize highp vec4 halo_color @@ -69,90 +50,39 @@ void main() { vec2 a_tex = a_data.xy; - highp vec2 label_data = unpack_float(a_data[2]); - highp float a_labelminzoom = label_data[0]; - highp float a_lineangle = (label_data[1] / 256.0 * 2.0 * PI); - highp vec2 a_zoom = unpack_float(a_data[3]); - highp float a_minzoom = a_zoom[0]; - highp float a_maxzoom = a_zoom[1]; + mediump vec2 label_data = unpack_float(a_data[2]); + mediump float a_labelminzoom = label_data[0]; float size; - // In order to accommodate placing labels around corners in - // symbol-placement: line, each glyph in a label could have multiple - // "quad"s only one of which should be shown at a given zoom level. - // The min/max zoom assigned to each quad is based on the font size at - // the vector tile's zoom level, which might be different than at the - // currently rendered zoom level if text-size is zoom-dependent. - // Thus, we compensate for this difference by calculating an adjustment - // based on the scale of rendered text size relative to layout text size. - highp float layoutSize; if (!u_is_size_zoom_constant && !u_is_size_feature_constant) { size = mix(a_size[0], a_size[1], u_size_t) / 10.0; - layoutSize = a_size[2] / 10.0; } else if (u_is_size_zoom_constant && !u_is_size_feature_constant) { size = a_size[0] / 10.0; - layoutSize = size; } else if (!u_is_size_zoom_constant && u_is_size_feature_constant) { size = u_size; - layoutSize = u_layout_size; } else { size = u_size; - layoutSize = u_size; } - float fontScale = u_is_text ? size / 24.0 : size; - - vec4 projectedPoint = u_matrix * vec4(a_label_pos, 0, 1); + vec4 projectedPoint = u_matrix * vec4(a_pos, 0, 1); highp float camera_to_anchor_distance = projectedPoint.w; - highp float perspective_ratio = 1.0 + 0.5*((camera_to_anchor_distance / u_camera_to_center_distance) - 1.0); - - // pitch-alignment: map - // rotation-alignment: map | viewport - if (u_pitch_with_map) { - highp float angle = u_rotate_with_map ? a_lineangle : u_bearing; - highp float asin = sin(angle); - highp float acos = cos(angle); - mat2 RotationMatrix = mat2(acos, asin, -1.0 * asin, acos); - vec2 offset = RotationMatrix * a_offset; - vec2 extrude = fontScale * u_extrude_scale * perspective_ratio * (offset / 64.0); - - gl_Position = u_matrix * vec4(a_pos + extrude, 0, 1); - gl_Position.z += clipUnusedGlyphAngles(size*perspective_ratio, layoutSize, a_minzoom, a_maxzoom) * gl_Position.w; - // pitch-alignment: viewport - // rotation-alignment: map - } else if (u_rotate_with_map) { - // foreshortening factor to apply on pitched maps - // as a label goes from horizontal <=> vertical in angle - // it goes from 0% foreshortening to up to around 70% foreshortening - highp float pitchfactor = 1.0 - cos(u_pitch * sin(u_pitch * 0.75)); - - // use the lineangle to position points a,b along the line - // project the points and calculate the label angle in projected space - // this calculation allows labels to be rendered unskewed on pitched maps - vec4 a = u_matrix * vec4(a_pos, 0, 1); - vec4 b = u_matrix * vec4(a_pos + vec2(cos(a_lineangle), sin(a_lineangle)), 0, 1); - highp float angle = atan((b[1] / b[3] - a[1] / a[3]) / u_aspect_ratio, b[0] / b[3] - a[0] / a[3]); - highp float asin = sin(angle); - highp float acos = cos(angle); - mat2 RotationMatrix = mat2(acos, -1.0 * asin, asin, acos); - highp float foreshortening = (1.0 - pitchfactor) + (pitchfactor * cos(angle * 2.0)); - - vec2 offset = RotationMatrix * (vec2(foreshortening, 1.0) * a_offset); - vec2 extrude = fontScale * u_extrude_scale * perspective_ratio * (offset / 64.0); - - gl_Position = u_matrix * vec4(a_pos, 0, 1) + vec4(extrude, 0, 0); - gl_Position.z += clipUnusedGlyphAngles(size * perspective_ratio, layoutSize, a_minzoom, a_maxzoom) * gl_Position.w; - // pitch-alignment: viewport - // rotation-alignment: viewport - } else { - vec2 extrude = fontScale * u_extrude_scale * perspective_ratio * (a_offset / 64.0); - gl_Position = u_matrix * vec4(a_pos, 0, 1) + vec4(extrude, 0, 0); - } + 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 = 0.5 + 0.5 * clamp(distance_ratio, 0.1, 10.0); + + size *= perspective_ratio; + + float fontScale = u_is_text ? size / 24.0 : size; - gl_Position.z += - step(u_max_camera_distance * u_camera_to_center_distance, camera_to_anchor_distance) * gl_Position.w; + highp float segment_angle = -a_projected_pos[2]; + highp float angle_sin = sin(segment_angle); + highp float angle_cos = cos(segment_angle); + mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos); - float gamma_scale = gl_Position.w / perspective_ratio; + vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0); + gl_Position = u_gl_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 64.0 * fontScale), 0.0, 1.0); + float gamma_scale = gl_Position.w; vec2 tex = a_tex / u_texsize; // incidence_stretch is the ratio of how much y space a label takes up on a tile while drawn perpendicular to the viewport vs @@ -174,7 +104,8 @@ void main() { highp float collision_adjustment = max(1.0, incidence_stretch / u_collision_y_stretch); // Floor to 1/10th zoom to dodge precision issues that can cause partially hidden labels - highp float perspective_zoom_adjust = floor(log2(perspective_ratio * collision_adjustment) * 10.0); + highp float collision_perspective_ratio = 1.0 + 0.5*((camera_to_anchor_distance / u_camera_to_center_distance) - 1.0); + highp float perspective_zoom_adjust = floor(log2(collision_perspective_ratio * collision_adjustment) * 10.0); vec2 fade_tex = vec2((a_labelminzoom + perspective_zoom_adjust) / 255.0, 0.0); v_data0 = vec4(tex.x, tex.y, fade_tex.x, fade_tex.y); diff --git a/src/symbol/projection.js b/src/symbol/projection.js new file mode 100644 index 00000000000..50fd3d1e0fc --- /dev/null +++ b/src/symbol/projection.js @@ -0,0 +1,243 @@ +'use strict'; + +const Point = require('point-geometry'); +const assert = require('assert'); +const mat4 = require('@mapbox/gl-matrix').mat4; +const vec4 = require('@mapbox/gl-matrix').vec4; +const symbolSize = require('./symbol_size'); + +const offscreenPoint = new Point(-Infinity, -Infinity); + +module.exports = { + updateLineLabels: updateLineLabels, + getLabelPlaneMatrix: getLabelPlaneMatrix, + getGlCoordMatrix: getGlCoordMatrix +}; + +/* + * # Overview of coordinate spaces + * + * ## Tile coordinate spaces + * Each label has an anchor. Some labels have corresponding line geometries. + * The points for both anchors and lines are stored in tile units. Each tile has it's own + * coordinate space going from (0, 0) at the top left to (EXTENT, EXTENT) at the bottom right. + * + * ## GL coordinate space + * At the end of everything, the vertex shader needs to produce a position in GL coordinate space, + * which is (-1, 1) at the top left and (1, -1) in the bottom left. + * + * ## Map pixel coordinate spaces + * Each tile has a pixel coordinate space. It's just the tile units scaled so that one unit is + * whatever counts as 1 pixel at the current zoom. + * This space is used for pitch-alignment=map, rotation-alignment=map + * + * ## Rotated map pixel coordinate spaces + * Like the above, but rotated so axis of the space are aligned with the viewport instead of the tile. + * This space is used for pitch-alignment=map, rotation-alignment=viewport + * + * ## Viewport pixel coordinate space + * (0, 0) is at the top left of the canvas and (pixelWidth, pixelHeight) is at the bottom right corner + * of the canvas. This space is used for pitch-alignment=viewport + * + * + * # Vertex projection + * It goes roughly like this: + * 1. project the anchor and line from tile units into the correct label coordinate space + * - map pixel space pitch-alignment=map rotation-alignment=map + * - rotated map pixel space pitch-alignment=map rotation-alignment=viewport + * - viewport pixel space pitch-alignment=viewport rotation-alignment=* + * 2. the the label follows a line, find the point along the line that is the correct distance from the anchor. + * 3. add the glyph's corner offset to the point from step 3 + * 4. convert from the label coordinate space to gl coordinates + * + * For horizontal labels we want to do step 1 in the shader for performance reasons (no cpu work). + * This is what `u_label_plane_matrix` is used for. + * For labels aligned with lines we have to steps 1 and 2 on the cpu since we need access to the line geometry. + * This is what `updateLineLabels(...)` does. + * Since the conversion is handled on the cpu we just set `u_label_plane_matrix` to an identity matrix. + * + * Steps 3 and 4 are done in the shaders for all labels. + */ + +/* + * Returns a matrix for converting from tile units to the correct label coordinate space. + */ +function getLabelPlaneMatrix(posMatrix, pitchWithMap, rotateWithMap, transform, pixelsToTileUnits) { + const m = mat4.identity(new Float32Array(16)); + if (pitchWithMap) { + mat4.identity(m); + mat4.scale(m, m, [1 / pixelsToTileUnits, 1 / pixelsToTileUnits, 1]); + if (!rotateWithMap) { + mat4.rotateZ(m, m, transform.angle); + } + } else { + mat4.scale(m, m, [transform.width / 2, -transform.height / 2, 1]); + mat4.translate(m, m, [1, -1, 0]); + mat4.multiply(m, m, posMatrix); + } + return m; +} + +/* + * Returns a matrix for converting from the correct label coordinate space to gl coords. + */ +function getGlCoordMatrix(posMatrix, pitchWithMap, rotateWithMap, transform, pixelsToTileUnits) { + const m = mat4.identity(new Float32Array(16)); + if (pitchWithMap) { + mat4.multiply(m, m, posMatrix); + mat4.scale(m, m, [pixelsToTileUnits, pixelsToTileUnits, 1]); + if (!rotateWithMap) { + mat4.rotateZ(m, m, -transform.angle); + } + } else { + mat4.scale(m, m, [1, -1, 1]); + mat4.translate(m, m, [-1, -1, 0]); + mat4.scale(m, m, [2 / transform.width, 2 / transform.height, 1]); + } + return m; +} + +function project(point, matrix) { + const pos = [point.x, point.y, 0, 1]; + vec4.transformMat4(pos, pos, matrix); + return new Point(pos[0] / pos[3], pos[1] / pos[3]); +} + +function isVisible(symbol, posMatrix, clippingBuffer, painter) { + const p = project(new Point(symbol.anchorX, symbol.anchorY), posMatrix); + const inPaddedViewport = ( + p.x >= -clippingBuffer[0] && + p.x <= clippingBuffer[0] && + p.y >= -clippingBuffer[1] && + p.y <= clippingBuffer[1]); + return inPaddedViewport && painter.frameHistory.isVisible(symbol.placementZoom); +} + +/* + * Update the `dynamicLayoutVertexBuffer` for the buffer with the correct glyph positions for the current map view. + * This is only run on labels that are aligned with lines. Horizontal labels are handled entirely in the shader. + */ +function updateLineLabels(bucket, posMatrix, painter, isText, labelPlaneMatrix, keepUpright, pixelsToTileUnits, layer) { + + const sizeData = isText ? bucket.textSizeData : bucket.iconSizeData; + const partiallyEvaluatedSize = symbolSize.evaluateSizeForZoom(sizeData, painter.transform, layer, isText); + + const clippingBuffer = [256 / painter.width * 2 + 1, 256 / painter.height * 2 + 1]; + + const dynamicLayoutVertexArray = isText ? + bucket.buffers.glyph.dynamicLayoutVertexArray : + bucket.buffers.icon.dynamicLayoutVertexArray; + dynamicLayoutVertexArray.clear(); + + const lineVertexArray = bucket.lineVertexArray; + const placedSymbols = isText ? bucket.placedGlyphArray : bucket.placedIconArray; + + for (let s = 0; s < placedSymbols.length; s++) { + const symbol = placedSymbols.get(s); + + // Don't bother calculating the correct point for invisible labels. Move them offscreen. + if (!isVisible(symbol, posMatrix, clippingBuffer, painter)) { + for (let i = symbol.numGlyphs; i > 0; i--) { + addGlyph(offscreenPoint, 0, dynamicLayoutVertexArray); + } + continue; + } + + // Determine whether the label needs to be flipped to keep it upright. + let flip = false; + if (keepUpright) { + const a = project(lineVertexArray.get(symbol.lineStartIndex + symbol.segment), posMatrix); + const b = project(lineVertexArray.get(symbol.lineStartIndex + symbol.segment + 1), posMatrix); + flip = symbol.vertical ? b.y > a.y : b.x < a.x; + } + + const fontSize = symbolSize.evaluateSizeForFeature(sizeData, partiallyEvaluatedSize, symbol); + const fontScale = fontSize / 24; + const offsetY = symbol.lineOffsetY * fontSize; + + const glyphsForward = []; + const glyphsBackward = []; + + const end = symbol.glyphStartIndex + symbol.numGlyphs; + for (let glyphIndex = symbol.glyphStartIndex; glyphIndex < end; glyphIndex++) { + const glyph = bucket.glyphOffsetArray.get(glyphIndex); + if (glyph.offsetX > 0) { + glyphsForward.push(glyph); + } else { + glyphsBackward.push(glyph); + } + } + processDirection(glyphsForward, 1, flip, symbol, offsetY, lineVertexArray, dynamicLayoutVertexArray, labelPlaneMatrix, fontScale); + processDirection(glyphsBackward, -1, flip, symbol, offsetY, lineVertexArray, dynamicLayoutVertexArray, labelPlaneMatrix, fontScale); + } + + if (isText) { + bucket.buffers.glyph.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray.serialize()); + } else { + bucket.buffers.icon.dynamicLayoutVertexBuffer.updateData(dynamicLayoutVertexArray.serialize()); + } +} + +function processDirection(glyphs, dir, flip, symbol, offsetY, lineVertexArray, dynamicLayoutVertexArray, labelPlaneMatrix, fontScale) { + assert(symbol.lineLength > 1); + + let prev = project(new Point(symbol.anchorX, symbol.anchorY), labelPlaneMatrix); + let next = prev; + let vertexIndex = 0; + let previousDistance = 0; + let segmentDistance = 0; + let segmentAngle = 0; + + let numVertices, vertexStartIndex; + let angle = 0; + + if (flip) { + // The label needs to be flipped to keep text upright. + // Iterate in the reverse direction. + dir *= -1; + angle = Math.PI; + } + + if (dir === 1) { + numVertices = symbol.lineLength - symbol.segment - 1; + vertexStartIndex = symbol.lineStartIndex + symbol.segment + 1; + } else { + numVertices = symbol.segment + 1; + vertexStartIndex = symbol.lineStartIndex + symbol.segment; + angle += Math.PI; + } + + // For each glyph, find the point `offsetX` distance from the anchor. + for (const glyph of glyphs) { + const offsetX = Math.abs(glyph.offsetX) * fontScale; + + // If the current segment doesn't have enough remaining space, iterate forward along the line. + // Since all the glyphs are sorted by their distance from the anchor you never need to iterate backwards. + // This way line vertices are projected at most once. + while (offsetX >= segmentDistance + previousDistance && Math.abs(vertexIndex) < numVertices) { + previousDistance += segmentDistance; + prev = next; + next = project(lineVertexArray.get(vertexStartIndex + vertexIndex), labelPlaneMatrix); + vertexIndex += dir; + segmentAngle = angle + Math.atan2(next.y - prev.y, next.x - prev.x); + segmentDistance = prev.dist(next); + } + + // The point is on the current segment. Interpolate to find it. + const segmentInterpolationT = (offsetX - previousDistance) / segmentDistance; + const prevToNext = next.sub(prev); + const p = prevToNext.mult(segmentInterpolationT)._add(prev); + + // offset the point from the line to text-offset and icon-offset + p._add(prevToNext._unit()._perp()._mult(offsetY * dir)); + + addGlyph(p, segmentAngle, dynamicLayoutVertexArray); + } +} + +function addGlyph(p, angle, dynamicLayoutVertexArray) { + dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle); + dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle); + dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle); + dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle); +} diff --git a/src/symbol/quads.js b/src/symbol/quads.js index c1120354d9c..a8fcc3df027 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -8,39 +8,28 @@ module.exports = { SymbolQuad: SymbolQuad }; -const minScale = 0.5; // underscale by 1 zoom level - /** * A textured quad for rendering a single icon or glyph. * * The zoom range the glyph can be shown is defined by minScale and maxScale. * - * @param {Point} anchorPoint the point the symbol is anchored around * @param {Point} tl The offset of the top left corner from the anchor. * @param {Point} tr The offset of the top right corner from the anchor. * @param {Point} bl The offset of the bottom left corner from the anchor. * @param {Point} br The offset of the bottom right corner from the anchor. * @param {Object} tex The texture coordinates. - * @param {number} anchorAngle The angle of the label at it's center, not the angle of this quad. - * @param {number} glyphAngle The angle of the glyph to be positioned in the quad. - * @param {number} minScale The minimum scale, relative to the tile's intended scale, that the glyph can be shown at. - * @param {number} maxScale The maximum scale, relative to the tile's intended scale, that the glyph can be shown at. * * @class SymbolQuad * @private */ -function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, minScale, maxScale, writingMode) { - this.anchorPoint = anchorPoint; +function SymbolQuad(tl, tr, bl, br, tex, writingMode, glyphOffset) { this.tl = tl; this.tr = tr; this.bl = bl; this.br = br; this.tex = tex; - this.anchorAngle = anchorAngle; - this.glyphAngle = glyphAngle; - this.minScale = minScale; - this.maxScale = maxScale; this.writingMode = writingMode; + this.glyphOffset = glyphOffset; } /** @@ -48,8 +37,6 @@ function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, m * * @param {Anchor} anchor * @param {PositionedIcon} shapedIcon - * @param {number} boxScale A magic number for converting glyph metric units to geometry units. - * @param {Array>} line * @param {StyleLayer} layer * @param {boolean} alongLine Whether the icon should be placed along the line. * @param {Shaping} shapedText Shaping for corresponding text @@ -58,7 +45,7 @@ function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, m * @returns {Array} * @private */ -function getIconQuads(anchor, shapedIcon, boxScale, line, layer, alongLine, shapedText, globalProperties, featureProperties) { +function getIconQuads(anchor, shapedIcon, layer, alongLine, shapedText, globalProperties, featureProperties) { const image = shapedIcon.image; const layout = layer.layout; @@ -104,26 +91,17 @@ function getIconQuads(anchor, shapedIcon, boxScale, line, layer, alongLine, shap bl = new Point(left, bottom); } - let angle = layer.getLayoutValue('icon-rotate', globalProperties, featureProperties) * Math.PI / 180; - if (alongLine) { - const prev = line[anchor.segment]; - if (anchor.y === prev.y && anchor.x === prev.x && anchor.segment + 1 < line.length) { - const next = line[anchor.segment + 1]; - angle += Math.atan2(anchor.y - next.y, anchor.x - next.x) + Math.PI; - } else { - angle += Math.atan2(anchor.y - prev.y, anchor.x - prev.x); - } - } + const angle = layer.getLayoutValue('icon-rotate', globalProperties, featureProperties) * Math.PI / 180; if (angle) { const sin = Math.sin(angle), cos = Math.cos(angle), matrix = [cos, -sin, sin, cos]; - tl = tl.matMult(matrix); - tr = tr.matMult(matrix); - bl = bl.matMult(matrix); - br = br.matMult(matrix); + tl._matMult(matrix); + tr._matMult(matrix); + bl._matMult(matrix); + br._matMult(matrix); } // Icon quad is padded, so texture coordinates also need to be padded. @@ -134,7 +112,7 @@ function getIconQuads(anchor, shapedIcon, boxScale, line, layer, alongLine, shap h: image.textureRect.h + border * 2 }; - return [new SymbolQuad(new Point(anchor.x, anchor.y), tl, tr, bl, br, textureRect, 0, 0, minScale, Infinity)]; + return [new SymbolQuad(tl, tr, bl, br, textureRect, undefined, [0, 0])]; } /** @@ -142,8 +120,6 @@ function getIconQuads(anchor, shapedIcon, boxScale, line, layer, alongLine, shap * * @param {Anchor} anchor * @param {Shaping} shaping - * @param {number} boxScale A magic number for converting from glyph metric units to geometry units. - * @param {Array>} line * @param {StyleLayer} layer * @param {boolean} alongLine Whether the label should be placed along the line. * @param {Object} globalProperties @@ -151,17 +127,15 @@ function getIconQuads(anchor, shapedIcon, boxScale, line, layer, alongLine, shap * @returns {Array} * @private */ -function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine, globalProperties, featureProperties) { +function getGlyphQuads(anchor, shaping, layer, alongLine, globalProperties, featureProperties) { const oneEm = 24; const textRotate = layer.getLayoutValue('text-rotate', globalProperties, featureProperties) * Math.PI / 180; - const keepUpright = layer.layout['text-keep-upright']; const textOffset = layer.getLayoutValue('text-offset', globalProperties, featureProperties).map((t)=> t * oneEm); const positionedGlyphs = shaping.positionedGlyphs; const quads = []; - let labelMinScale = minScale; for (let k = 0; k < positionedGlyphs.length; k++) { const positionedGlyph = positionedGlyphs[k]; @@ -171,294 +145,64 @@ function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine, global const rect = glyph.rect; if (!rect) continue; - const centerX = (positionedGlyph.x + glyph.advance / 2) * boxScale; - - let glyphInstances; - if (alongLine) { - glyphInstances = []; - labelMinScale = Math.max(labelMinScale, getLineGlyphs(glyphInstances, anchor, centerX, line, anchor.segment, false)); - if (keepUpright) { - labelMinScale = Math.max(labelMinScale, getLineGlyphs(glyphInstances, anchor, centerX, line, anchor.segment, true)); - } + const halfAdvance = glyph.advance / 2; - } else { - glyphInstances = [{ - anchorPoint: new Point(anchor.x, anchor.y), - upsideDown: false, - angle: 0, - maxScale: Infinity, - minScale: minScale - }]; - } - - const baseQuad = { - upright: calculateBaseQuad(positionedGlyph, glyph, rect, textOffset), - // The quad coordinates represent an offset from the anchor. Since - // we use the same anchor for both the 'upright' and 'upside-down' - // copies of each glyph, invert the y dimension of text-offset for the - // upside-down case. - upsideDown: calculateBaseQuad(positionedGlyph, glyph, rect, [textOffset[0], -textOffset[1]]) - }; + const glyphOffset = alongLine ? + [positionedGlyph.x + halfAdvance, positionedGlyph.y] : + [0, 0]; - for (let i = 0; i < glyphInstances.length; i++) { + const builtInOffset = alongLine ? + [0, 0] : + [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1]]; - const instance = glyphInstances[i]; - const base = baseQuad[instance.upsideDown ? 'upsideDown' : 'upright']; - let tl = base.tl, - tr = base.tr, - bl = base.bl, - br = base.br; - if (textRotate) { - const sin = Math.sin(textRotate), - cos = Math.cos(textRotate), - matrix = [cos, -sin, sin, cos]; + const x1 = glyph.left - halfAdvance + builtInOffset[0]; + const y1 = -glyph.top + builtInOffset[1]; + const x2 = x1 + rect.w; + const y2 = y1 + rect.h; - tl = tl.matMult(matrix); - tr = tr.matMult(matrix); - bl = bl.matMult(matrix); - br = br.matMult(matrix); - } + const tl = new Point(x1, y1); + const tr = new Point(x2, y1); + const bl = new Point(x1, y2); + const br = new Point(x2, y2); - // Prevent label from extending past the end of the line - const glyphMinScale = Math.max(instance.minScale, labelMinScale); - // All the glyphs for a label are tagged with either the "right side up" or "upside down" anchor angle, - // which is used at placement time to determine which set to show - const anchorAngle = (anchor.angle + (instance.upsideDown ? Math.PI : 0.0) + 2 * Math.PI) % (2 * Math.PI); - const glyphAngle = (instance.angle + (instance.upsideDown ? Math.PI : 0.0) + 2 * Math.PI) % (2 * Math.PI); - quads.push(new SymbolQuad(instance.anchorPoint, tl, tr, bl, br, rect, anchorAngle, glyphAngle, glyphMinScale, instance.maxScale, shaping.writingMode)); + const center = new Point(builtInOffset[0] - halfAdvance, glyph.advance / 2); + if (positionedGlyph.angle !== 0) { + tl._sub(center)._rotate(positionedGlyph.angle)._add(center); + tr._sub(center)._rotate(positionedGlyph.angle)._add(center); + bl._sub(center)._rotate(positionedGlyph.angle)._add(center); + br._sub(center)._rotate(positionedGlyph.angle)._add(center); } - } - - return quads; -} - -function calculateBaseQuad(positionedGlyph, glyph, rect, offset) { - const x1 = positionedGlyph.x + glyph.left + offset[0]; - const y1 = positionedGlyph.y - glyph.top + offset[1]; - const x2 = x1 + rect.w; - const y2 = y1 + rect.h; - - const center = new Point(positionedGlyph.x, glyph.advance / 2); - - 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 (positionedGlyph.angle !== 0) { - tl._sub(center)._rotate(positionedGlyph.angle)._add(center); - tr._sub(center)._rotate(positionedGlyph.angle)._add(center); - bl._sub(center)._rotate(positionedGlyph.angle)._add(center); - br._sub(center)._rotate(positionedGlyph.angle)._add(center); - } - - return { tl, tr, bl, br }; -} + if (textRotate) { + const sin = Math.sin(textRotate), + cos = Math.cos(textRotate), + matrix = [cos, -sin, sin, cos]; - -/** - * We can only render glyph quads that slide along a straight line. To draw - * curved lines we need an instance of a glyph for each segment it appears on. - * This creates all the instances of a glyph that are necessary to render a label. - * - * Given (1) a glyph positioned relative to an anchor point and (2) a line to follow, - * calculates which segment of the line the glyph will fall on for each possible - * scale range, and for each range produces a "virtual" anchor point and an angle that will - * place the glyph on the right segment and rotated to the correct angle. - * - * Because one glyph quad is made ahead of time for each possible orientation, the - * symbol_sdf shader can quickly handle changing layout as we zoom in and out - * - * If the "keepUpright" property is set, we call getLineGlyphs twice (once upright and - * once "upside down"). This will generate two sets of glyphs following the line in opposite - * directions. Later, SymbolLayout::place will look at the glyphs and based on the placement - * angle determine if their original anchor was "upright" or not -- based on that, it throws - * away one set of glyphs or the other (this work has to be done in the CPU, but it's just a - * filter so it's fast) - * - * We need a - * @param {Array} glyphs An empty array that glyphInstances are added to. - * @param {Anchor} anchor - * @param {number} glyphHorizontalOffsetFromAnchor The glyph's offset from the center of the label. - * @param {Array} line - * @param {number} anchorSegment The index of the segment of the line on which the anchor exists. - * @param {boolean} upsideDown - * - * @returns {number} minScale - * @private - */ -function getLineGlyphs(glyphs, anchor, glyphHorizontalOffsetFromAnchor, line, anchorSegment, upsideDown) { - - // This is true if the glyph is "logically forward" of the anchor point, based on the ordering of line segments - // The actual angle of the line is irrelevant - // If "upsideDown" is set, everything is flipped - const glyphIsLogicallyForward = (glyphHorizontalOffsetFromAnchor >= 0) ^ upsideDown; - const glyphDistanceFromAnchor = Math.abs(glyphHorizontalOffsetFromAnchor); - - const initialSegmentAnchor = new Point(anchor.x, anchor.y); - const initialSegmentEnd = getSegmentEnd(glyphIsLogicallyForward, line, anchorSegment); - - let virtualSegment = { - anchor: initialSegmentAnchor, - end: initialSegmentEnd, - index: anchorSegment, - minScale: getMinScaleForSegment(glyphDistanceFromAnchor, initialSegmentAnchor, initialSegmentEnd), - maxScale: Infinity - }; - - while (true) { - insertSegmentGlyph(glyphs, - virtualSegment, - glyphIsLogicallyForward, - upsideDown); - - if (virtualSegment.minScale <= anchor.scale) { - // No need to calculate below the scale where the label starts showing - return anchor.scale; + tl._matMult(matrix); + tr._matMult(matrix); + bl._matMult(matrix); + br._matMult(matrix); } - const nextVirtualSegment = getNextVirtualSegment(virtualSegment, - line, - glyphDistanceFromAnchor, - glyphIsLogicallyForward); - if (!nextVirtualSegment) { - // There are no more segments, so we can't fit this glyph on the line at a lower scale - // This implies we can't show the label at all at lower scale, so we update the anchor's min scale - return virtualSegment.minScale; - } else { - virtualSegment = nextVirtualSegment; - } + quads.push(new SymbolQuad(tl, tr, bl, br, rect, shaping.writingMode, glyphOffset)); } -} - -/** - * @param {Array} glyphs - * @param {Object} virtualSegment - * @param {boolean} glyphIsLogicallyForward - * @param {boolean} upsideDown - * @private - */ -function insertSegmentGlyph(glyphs, virtualSegment, glyphIsLogicallyForward, upsideDown) { - const segmentAngle = Math.atan2(virtualSegment.end.y - virtualSegment.anchor.y, virtualSegment.end.x - virtualSegment.anchor.x); - // If !glyphIsLogicallyForward, we're iterating through the segments in reverse logical order as well, so we need to flip the segment angle - const glyphAngle = glyphIsLogicallyForward ? segmentAngle : segmentAngle + Math.PI; - - // Insert a glyph rotated at this angle for display in the range from [scale, previous(larger) scale]. - glyphs.push({ - anchorPoint: virtualSegment.anchor, - upsideDown: upsideDown, - minScale: virtualSegment.minScale, - maxScale: virtualSegment.maxScale, - angle: (glyphAngle + 2.0 * Math.PI) % (2.0 * Math.PI)}); -} - -/** - * Given the distance along the line from the label anchor to the beginning of the current segment, - * project a "virtual anchor" point at the same distance along the line extending out from this segment. - * - * B <-- beginning of current segment - * * . . . . . . . *--------* E <-- end of current segment - * VA | - * / VA = "virtual segment anchor" - * / - * ---*-----` - * A = label anchor - * - * Distance _along line_ from A to B == straight-line distance from VA to B. - * - * @param {Point} segmentBegin - * @param {Point} segmentEnd - * @param {number} distanceFromAnchorToSegmentBegin - * - * @returns {Point} virtualSegmentAnchor - * @private - */ -function getVirtualSegmentAnchor(segmentBegin, segmentEnd, distanceFromAnchorToSegmentBegin) { - const segmentDirectionUnitVector = segmentEnd.sub(segmentBegin)._unit(); - return segmentBegin.sub(segmentDirectionUnitVector._mult(distanceFromAnchorToSegmentBegin)); -} - -/** - * Given the segment joining `segmentAnchor` and `segmentEnd` and a desired offset - * `glyphDistanceFromAnchor` at which a glyph is to be placed, calculate the minimum - * "scale" at which the glyph will fall on the segment (i.e., not past the end) - * - * "Scale" here refers to the ratio between the *rendered* zoom level and the text-layout - * zoom level, which is 1 + (source tile's zoom level). `glyphDistanceFromAnchor`, although - * passed in units consistent with the text-layout zoom level, is based on text size. So - * when the tile is being rendered at z < text-layout zoom, the glyph's actual distance from - * the anchor is larger relative to the segment's length than at layout time: - * - * - * GLYPH - * z == layout-zoom, scale == 1: segmentAnchor *--------------^-------------* segmentEnd - * z == layout-zoom - 1, scale == 0.5: segmentAnchor *--------------^* segmentEnd - * - * <--------------> - * Anchor-to-glyph distance stays visually fixed, - * so it changes relative to the segment. - * @param {number} glyphDistanceFromAnchor - * @param {Point} segmentAnchor - * @param {Point} segmentEnd - * @returns {number} minScale - * @private - */ -function getMinScaleForSegment(glyphDistanceFromAnchor, segmentAnchor, segmentEnd) { - const distanceFromAnchorToEnd = segmentAnchor.dist(segmentEnd); - return glyphDistanceFromAnchor / distanceFromAnchorToEnd; -} - -/** - * @param {boolean} glyphIsLogicallyForward - * @param {Array} line - * @param {number} segmentIndex - * - * @returns {Point} segmentEnd - * @private - */ -function getSegmentEnd(glyphIsLogicallyForward, line, segmentIndex) { - return glyphIsLogicallyForward ? line[segmentIndex + 1] : line[segmentIndex]; -} - -/** - * @param {Object} previousVirtualSegment - * @param {Array} line - * @param {number} glyphDistanceFromAnchor - * @param {boolean} glyphIsLogicallyForward - - * @returns {Object} virtualSegment - * @private - */ -function getNextVirtualSegment(previousVirtualSegment, line, glyphDistanceFromAnchor, glyphIsLogicallyForward) { - const nextSegmentBegin = previousVirtualSegment.end; - - let end = nextSegmentBegin; - let index = previousVirtualSegment.index; - - // skip duplicate nodes - while (end.equals(nextSegmentBegin)) { - // look ahead by 2 points in the line because the segment index refers to the beginning - // of the segment, and we need an endpoint too - if (glyphIsLogicallyForward && (index + 2 < line.length)) { - index += 1; - } else if (!glyphIsLogicallyForward && index !== 0) { - index -= 1; + // Quads need to be in a strict order so that the render-time projection algorithm can be more efficient. + quads.sort((qa, qb) => { + const a = qa.glyphOffset[0]; + const b = qb.glyphOffset[0]; + const aIsForward = a > 0; + const bIsForward = b > 0; + + if (aIsForward === bIsForward) { + return Math.abs(a) - Math.abs(b); + } else if (aIsForward) { + return -1; } else { - return null; + return 1; } + }); - end = getSegmentEnd(glyphIsLogicallyForward, line, index); - } - - const anchor = getVirtualSegmentAnchor(nextSegmentBegin, end, - previousVirtualSegment.anchor.dist(previousVirtualSegment.end)); - return { - anchor: anchor, - end: end, - index: index, - minScale: getMinScaleForSegment(glyphDistanceFromAnchor, anchor, end), - maxScale: previousVirtualSegment.minScale - }; + return quads; } diff --git a/src/symbol/symbol_size.js b/src/symbol/symbol_size.js new file mode 100644 index 00000000000..86dc88e7f6f --- /dev/null +++ b/src/symbol/symbol_size.js @@ -0,0 +1,66 @@ +'use strict'; + +const interpolate = require('../style-spec/util/interpolate'); +const util = require('../util/util'); +const interpolationFactor = require('../style-spec/function').interpolationFactor; +const assert = require('assert'); + +module.exports = { + evaluateSizeForFeature: evaluateSizeForFeature, + evaluateSizeForZoom: evaluateSizeForZoom +}; + + +function evaluateSizeForFeature(sizeData, partiallyEvaluatedSize, symbol) { + const part = partiallyEvaluatedSize; + if (sizeData.isFeatureConstant) { + return part.uSize; + } else { + if (sizeData.isZoomConstant) { + return symbol.lowerSize / 10; + } else { + return interpolate.number(symbol.lowerSize / 10, symbol.upperSize / 10, part.uSizeT); + } + } +} + +function evaluateSizeForZoom(sizeData, tr, layer, isText) { + const sizeUniforms = {}; + if (!sizeData.isZoomConstant && !sizeData.isFeatureConstant) { + // composite function + const t = interpolationFactor(tr.zoom, + sizeData.functionBase, + sizeData.coveringZoomRange[0], + sizeData.coveringZoomRange[1] + ); + sizeUniforms.uSizeT = util.clamp(t, 0, 1); + } else if (sizeData.isFeatureConstant && !sizeData.isZoomConstant) { + // camera function + let size; + if (sizeData.functionType === 'interval') { + size = layer.getLayoutValue(isText ? 'text-size' : 'icon-size', + {zoom: tr.zoom}); + } else { + assert(sizeData.functionType === 'exponential'); + // Even though we could get the exact value of the camera function + // at z = tr.zoom, we intentionally do not: instead, we interpolate + // between the camera function values at a pair of zoom stops covering + // [tileZoom, tileZoom + 1] in order to be consistent with this + // restriction on composite functions + const t = sizeData.functionType === 'interval' ? 0 : + interpolationFactor(tr.zoom, + sizeData.functionBase, + sizeData.coveringZoomRange[0], + sizeData.coveringZoomRange[1]); + + const lowerValue = sizeData.coveringStopValues[0]; + const upperValue = sizeData.coveringStopValues[1]; + size = lowerValue + (upperValue - lowerValue) * util.clamp(t, 0, 1); + } + + sizeUniforms.uSize = size; + } else if (sizeData.isFeatureConstant && sizeData.isZoomConstant) { + sizeUniforms.uSize = sizeData.layoutSize; + } + return sizeUniforms; +} diff --git a/src/util/struct_array.js b/src/util/struct_array.js index a119b2c5e99..6b4e2f78f17 100644 --- a/src/util/struct_array.js +++ b/src/util/struct_array.js @@ -158,6 +158,13 @@ class StructArray { } } + /** + * Resets the the length of the array to 0 without de-allocating capcacity. + */ + clear() { + this.length = 0; + } + /** * Resize the array. * If `n` is greater than the current length then additional elements with undefined values are added. @@ -294,6 +301,9 @@ function createStructArrayType(options: {| for (const member of members) { for (let c = 0; c < member.components; c++) { const name = member.name + (member.components === 1 ? '' : c); + if (name in StructType.prototype) { + throw new Error(`${name} is a reserved name and cannot be used as a member name.`); + } Object.defineProperty(StructType.prototype, name, { get: createGetter(member, c), set: createSetter(member, c) diff --git a/test/integration/render-tests/debug/collision-pitched-wrapped/expected.png b/test/integration/render-tests/debug/collision-pitched-wrapped/expected.png index c1ff274b1fe..9d0e9e34ba4 100644 Binary files a/test/integration/render-tests/debug/collision-pitched-wrapped/expected.png and b/test/integration/render-tests/debug/collision-pitched-wrapped/expected.png differ diff --git a/test/integration/render-tests/debug/collision-pitched/expected.png b/test/integration/render-tests/debug/collision-pitched/expected.png index c38a733eea1..bdbf42ebab2 100644 Binary files a/test/integration/render-tests/debug/collision-pitched/expected.png and b/test/integration/render-tests/debug/collision-pitched/expected.png differ diff --git a/test/integration/render-tests/icon-text-fit/placement-line/expected.png b/test/integration/render-tests/icon-text-fit/placement-line/expected.png index a26b210d05a..f65ef8f89a2 100644 Binary files a/test/integration/render-tests/icon-text-fit/placement-line/expected.png and b/test/integration/render-tests/icon-text-fit/placement-line/expected.png differ diff --git a/test/integration/render-tests/symbol-placement/point-polygon/expected.png b/test/integration/render-tests/symbol-placement/point-polygon/expected.png index 11926866553..71430af2e35 100644 Binary files a/test/integration/render-tests/symbol-placement/point-polygon/expected.png and b/test/integration/render-tests/symbol-placement/point-polygon/expected.png differ diff --git a/test/integration/render-tests/symbol-spacing/line-close/expected.png b/test/integration/render-tests/symbol-spacing/line-close/expected.png index 26e569804a2..24f7a0fea3a 100644 Binary files a/test/integration/render-tests/symbol-spacing/line-close/expected.png and b/test/integration/render-tests/symbol-spacing/line-close/expected.png differ diff --git a/test/integration/render-tests/symbol-spacing/line-overscaled/expected.png b/test/integration/render-tests/symbol-spacing/line-overscaled/expected.png index c03be10dd71..9240bb54adf 100644 Binary files a/test/integration/render-tests/symbol-spacing/line-overscaled/expected.png and b/test/integration/render-tests/symbol-spacing/line-overscaled/expected.png differ diff --git a/test/integration/render-tests/text-keep-upright/line-placement-false/expected.png b/test/integration/render-tests/text-keep-upright/line-placement-false/expected.png index ece1593af51..6a22b29bb9a 100644 Binary files a/test/integration/render-tests/text-keep-upright/line-placement-false/expected.png and b/test/integration/render-tests/text-keep-upright/line-placement-false/expected.png differ diff --git a/test/integration/render-tests/text-keep-upright/line-placement-true-rotated/expected.png b/test/integration/render-tests/text-keep-upright/line-placement-true-rotated/expected.png index 353c933fe0a..2dabd50313c 100644 Binary files a/test/integration/render-tests/text-keep-upright/line-placement-true-rotated/expected.png and b/test/integration/render-tests/text-keep-upright/line-placement-true-rotated/expected.png differ diff --git a/test/integration/render-tests/text-keep-upright/line-placement-true/expected.png b/test/integration/render-tests/text-keep-upright/line-placement-true/expected.png index bc5787184bf..b4a064d61cc 100644 Binary files a/test/integration/render-tests/text-keep-upright/line-placement-true/expected.png and b/test/integration/render-tests/text-keep-upright/line-placement-true/expected.png differ diff --git a/test/integration/render-tests/text-max-angle/literal/expected.png b/test/integration/render-tests/text-max-angle/literal/expected.png index 3ea8192c5e9..c4b018c3386 100644 Binary files a/test/integration/render-tests/text-max-angle/literal/expected.png and b/test/integration/render-tests/text-max-angle/literal/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/auto-text-rotation-alignment-map/expected.png b/test/integration/render-tests/text-pitch-alignment/auto-text-rotation-alignment-map/expected.png index 01f7a0f698d..6d16ca1672e 100644 Binary files a/test/integration/render-tests/text-pitch-alignment/auto-text-rotation-alignment-map/expected.png and b/test/integration/render-tests/text-pitch-alignment/auto-text-rotation-alignment-map/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/map-text-depthtest/expected.png b/test/integration/render-tests/text-pitch-alignment/map-text-depthtest/expected.png index 34fc7bd0e05..caa3293ae4d 100644 Binary files a/test/integration/render-tests/text-pitch-alignment/map-text-depthtest/expected.png and b/test/integration/render-tests/text-pitch-alignment/map-text-depthtest/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map/expected.png b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map/expected.png index 01f7a0f698d..6d16ca1672e 100644 Binary files a/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map/expected.png and b/test/integration/render-tests/text-pitch-alignment/map-text-rotation-alignment-map/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/viewport-text-depthtest/expected.png b/test/integration/render-tests/text-pitch-alignment/viewport-text-depthtest/expected.png index 992481538a7..90fb148eda8 100644 Binary files a/test/integration/render-tests/text-pitch-alignment/viewport-text-depthtest/expected.png and b/test/integration/render-tests/text-pitch-alignment/viewport-text-depthtest/expected.png differ diff --git a/test/integration/render-tests/text-pitch-alignment/viewport-text-rotation-alignment-map/expected.png b/test/integration/render-tests/text-pitch-alignment/viewport-text-rotation-alignment-map/expected.png index 8d2024a709e..e946401c358 100644 Binary files a/test/integration/render-tests/text-pitch-alignment/viewport-text-rotation-alignment-map/expected.png and b/test/integration/render-tests/text-pitch-alignment/viewport-text-rotation-alignment-map/expected.png differ diff --git a/test/integration/render-tests/text-pitch-scaling/line-half/expected.png b/test/integration/render-tests/text-pitch-scaling/line-half/expected.png index b4ea1a351df..355de31bb3f 100644 Binary files a/test/integration/render-tests/text-pitch-scaling/line-half/expected.png and b/test/integration/render-tests/text-pitch-scaling/line-half/expected.png differ diff --git a/test/integration/render-tests/text-size/composite-function-line-placement/expected.png b/test/integration/render-tests/text-size/composite-function-line-placement/expected.png index 42656388d95..f8b3fa475bb 100644 Binary files a/test/integration/render-tests/text-size/composite-function-line-placement/expected.png and b/test/integration/render-tests/text-size/composite-function-line-placement/expected.png differ diff --git a/test/integration/render-tests/text-writing-mode/chinese/expected.png b/test/integration/render-tests/text-writing-mode/chinese/expected.png index 772c2839ea9..0202d5bbf16 100644 Binary files a/test/integration/render-tests/text-writing-mode/chinese/expected.png and b/test/integration/render-tests/text-writing-mode/chinese/expected.png differ diff --git a/test/integration/render-tests/text-writing-mode/mixed/expected.png b/test/integration/render-tests/text-writing-mode/mixed/expected.png index 8783724f740..ce48e76cbb8 100644 Binary files a/test/integration/render-tests/text-writing-mode/mixed/expected.png and b/test/integration/render-tests/text-writing-mode/mixed/expected.png differ diff --git a/test/unit/symbol/quads.test.js b/test/unit/symbol/quads.test.js index 5f43884af65..dda26fb3a21 100644 --- a/test/unit/symbol/quads.test.js +++ b/test/unit/symbol/quads.test.js @@ -4,7 +4,6 @@ const test = require('mapbox-gl-js-test').test; const getIconQuads = require('../../../src/symbol/quads').getIconQuads; const Anchor = require('../../../src/symbol/anchor'); -const Point = require('point-geometry'); function createLayer(layer) { return { @@ -35,19 +34,15 @@ test('getIconQuads', (t) => { const layer = createLayer({ layout: {'icon-rotate': 0} }); - t.deepEqual(getIconQuads(anchor, createShapedIcon(), 2, [], layer, false), [ + t.deepEqual(getIconQuads(anchor, createShapedIcon(), layer, false), [ { - anchorPoint: { x: 2, y: 3 }, tl: { x: -8, y: -6 }, tr: { x: 9, y: -6 }, bl: { x: -8, y: 7 }, br: { x: 9, y: 7 }, tex: { x: 0, y: 0, w: 17, h: 13 }, - anchorAngle: 0, - glyphAngle: 0, - minScale: 0.5, - maxScale: Infinity, - writingMode: null + writingMode: null, + glyphOffset: [0, 0] }]); t.end(); }); @@ -57,19 +52,15 @@ test('getIconQuads', (t) => { const layer = createLayer({ layout: {'icon-rotate': 0} }); - t.deepEqual(getIconQuads(anchor, createShapedIcon(), 2, [new Point(0, 0), new Point(8, 9)], layer, false), [ + t.deepEqual(getIconQuads(anchor, createShapedIcon(), layer, false), [ { - anchorPoint: { x: 2, y: 3}, tl: { x: -8, y: -6 }, tr: { x: 9, y: -6 }, bl: { x: -8, y: 7 }, br: { x: 9, y: 7 }, tex: { x: 0, y: 0, w: 17, h: 13 }, - anchorAngle: 0, - glyphAngle: 0, - minScale: 0.5, - maxScale: Infinity, - writingMode: null + writingMode: null, + glyphOffset: [0, 0] }]); t.end(); }); @@ -101,7 +92,7 @@ test('getIconQuads text-fit', (t) => { } t.test('icon-text-fit: none', (t) => { - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'icon-text-fit': 'none' } @@ -111,7 +102,7 @@ test('getIconQuads text-fit', (t) => { t.deepEqual(quads[0].bl, { x: -11, y: 11 }); t.deepEqual(quads[0].br, { x: 11, y: 11 }); - t.deepEqual(quads, getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + t.deepEqual(quads, getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'icon-text-fit': 'none', 'icon-text-fit-padding': [10, 10] @@ -124,7 +115,7 @@ test('getIconQuads text-fit', (t) => { t.test('icon-text-fit: width', (t) => { // - Uses text width // - Preserves icon height, centers vertically - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 24, 'icon-text-fit': 'width', @@ -141,7 +132,7 @@ test('getIconQuads text-fit', (t) => { t.test('icon-text-fit: width, x textSize', (t) => { // - Uses text width (adjusted for textSize) // - Preserves icon height, centers vertically - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 12, 'icon-text-fit': 'width', @@ -159,7 +150,7 @@ test('getIconQuads text-fit', (t) => { // - Uses text width (adjusted for textSize) // - Preserves icon height, centers vertically // - Applies padding x, padding y - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 12, 'icon-text-fit': 'width', @@ -176,7 +167,7 @@ test('getIconQuads text-fit', (t) => { t.test('icon-text-fit: height', (t) => { // - Uses text height // - Preserves icon width, centers horizontally - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 24, 'icon-text-fit': 'height', @@ -193,7 +184,7 @@ test('getIconQuads text-fit', (t) => { t.test('icon-text-fit: height, x textSize', (t) => { // - Uses text height (adjusted for textSize) // - Preserves icon width, centers horizontally - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 12, 'icon-text-fit': 'height', @@ -211,7 +202,7 @@ test('getIconQuads text-fit', (t) => { // - Uses text height (adjusted for textSize) // - Preserves icon width, centers horizontally // - Applies padding x, padding y - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 12, 'icon-text-fit': 'height', @@ -227,7 +218,7 @@ test('getIconQuads text-fit', (t) => { t.test('icon-text-fit: both', (t) => { // - Uses text width + height - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 24, 'icon-text-fit': 'both', @@ -243,7 +234,7 @@ test('getIconQuads text-fit', (t) => { t.test('icon-text-fit: both, x textSize', (t) => { // - Uses text width + height (adjusted for textSize) - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 12, 'icon-text-fit': 'both', @@ -260,7 +251,7 @@ test('getIconQuads text-fit', (t) => { t.test('icon-text-fit: both, x textSize, + padding', (t) => { // - Uses text width + height (adjusted for textSize) // - Applies padding x, padding y - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 12, 'icon-text-fit': 'both', @@ -277,7 +268,7 @@ test('getIconQuads text-fit', (t) => { t.test('icon-text-fit: both, padding t/r/b/l', (t) => { // - Uses text width + height (adjusted for textSize) // - Applies padding t/r/b/l - const quads = getIconQuads(anchor, createShapedIcon(), 2, [], createLayer({ + const quads = getIconQuads(anchor, createShapedIcon(), createLayer({ layout: { 'text-size': 12, 'icon-text-fit': 'both',