diff --git a/build/generate-struct-arrays.js b/build/generate-struct-arrays.js index 8238f98d5cb..6a3e8cf86ea 100644 --- a/build/generate-struct-arrays.js +++ b/build/generate-struct-arrays.js @@ -128,6 +128,7 @@ import fillAttributes from '../src/data/bucket/fill_attributes.js'; import lineAttributes from '../src/data/bucket/line_attributes.js'; import lineAttributesExt from '../src/data/bucket/line_attributes_ext.js'; import patternAttributes from '../src/data/bucket/pattern_attributes.js'; +import dashAttributes from '../src/data/bucket/dash_attributes.js'; import skyboxAttributes from '../src/render/skybox_attributes.js'; import {fillExtrusionAttributes, centroidAttributes} from '../src/data/bucket/fill_extrusion_attributes.js'; @@ -139,7 +140,8 @@ const layoutAttributes = { heatmap: circleAttributes, line: lineAttributes, lineExt: lineAttributesExt, - pattern: patternAttributes + pattern: patternAttributes, + dash: dashAttributes }; for (const name in layoutAttributes) { createStructArrayType(`${name.replace(/-/g, '_')}_layout`, layoutAttributes[name]); diff --git a/debug/dasharray.html b/debug/dasharray.html new file mode 100644 index 00000000000..be1b7d2590d --- /dev/null +++ b/debug/dasharray.html @@ -0,0 +1,72 @@ + + + + Mapbox GL JS debug page + + + + + + + +
+ + + + + + diff --git a/src/data/array_types.js b/src/data/array_types.js index 220dae362c5..3e673069f71 100644 --- a/src/data/array_types.js +++ b/src/data/array_types.js @@ -188,6 +188,44 @@ class StructArrayLayout10ui20 extends StructArray { StructArrayLayout10ui20.prototype.bytesPerElement = 20; register('StructArrayLayout10ui20', StructArrayLayout10ui20); +/** + * Implementation of the StructArray layout: + * [0]: Uint16[8] + * + * @private + */ +class StructArrayLayout8ui16 extends StructArray { + uint8: Uint8Array; + uint16: Uint16Array; + + _refreshViews() { + this.uint8 = new Uint8Array(this.arrayBuffer); + this.uint16 = new Uint16Array(this.arrayBuffer); + } + + emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) { + const i = this.length; + this.resize(i + 1); + return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7); + } + + emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) { + const o2 = i * 8; + this.uint16[o2 + 0] = v0; + this.uint16[o2 + 1] = v1; + this.uint16[o2 + 2] = v2; + this.uint16[o2 + 3] = v3; + this.uint16[o2 + 4] = v4; + this.uint16[o2 + 5] = v5; + this.uint16[o2 + 6] = v6; + this.uint16[o2 + 7] = v7; + return i; + } +} + +StructArrayLayout8ui16.prototype.bytesPerElement = 16; +register('StructArrayLayout8ui16', StructArrayLayout8ui16); + /** * Implementation of the StructArray layout: * [0]: Int16[4] @@ -1104,6 +1142,7 @@ export { StructArrayLayout2i4ub1f12, StructArrayLayout2f8, StructArrayLayout10ui20, + StructArrayLayout8ui16, StructArrayLayout4i4ui4i24, StructArrayLayout3f12, StructArrayLayout1ul4, @@ -1129,6 +1168,7 @@ export { StructArrayLayout2i4ub1f12 as LineLayoutArray, StructArrayLayout2f8 as LineExtLayoutArray, StructArrayLayout10ui20 as PatternLayoutArray, + StructArrayLayout8ui16 as DashLayoutArray, StructArrayLayout4i4ui4i24 as SymbolLayoutArray, StructArrayLayout3f12 as SymbolDynamicLayoutArray, StructArrayLayout1ul4 as SymbolOpacityArray, diff --git a/src/data/bucket.js b/src/data/bucket.js index 332f635da8d..b2c5c1a47d0 100644 --- a/src/data/bucket.js +++ b/src/data/bucket.js @@ -7,6 +7,7 @@ import type FeatureIndex from './feature_index.js'; import type Context from '../gl/context.js'; import type {FeatureStates} from '../source/source_state.js'; import type {ImagePosition} from '../render/image_atlas.js'; +import type LineAtlas from '../render/line_atlas.js'; import type {CanonicalTileID} from '../source/tile_id.js'; export type BucketParameters = { @@ -26,7 +27,8 @@ export type PopulateParameters = { iconDependencies: {}, patternDependencies: {}, glyphDependencies: {}, - availableImages: Array + availableImages: Array, + lineAtlas: LineAtlas } export type IndexedFeature = { diff --git a/src/data/bucket/dash_attributes.js b/src/data/bucket/dash_attributes.js new file mode 100644 index 00000000000..1b0520cf47a --- /dev/null +++ b/src/data/bucket/dash_attributes.js @@ -0,0 +1,9 @@ +// @flow +import {createLayout} from '../../util/struct_array.js'; + +const dashAttributes = createLayout([ + {name: 'a_dash_to', components: 4, type: 'Uint16'}, // [x, y, width, unused] + {name: 'a_dash_from', components: 4, type: 'Uint16'} +]); + +export default dashAttributes; diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index 5389cbe1dd5..0c620c747dc 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -34,6 +34,7 @@ import type IndexBuffer from '../../gl/index_buffer.js'; import type VertexBuffer from '../../gl/vertex_buffer.js'; import type {FeatureStates} from '../../source/source_state.js'; import type {ImagePosition} from '../../render/image_atlas.js'; +import type LineAtlas from '../../render/line_atlas.js'; // NOTE ON EXTRUDE SCALE: // scale the extrusion vector so that the normal length is this value. @@ -169,21 +170,100 @@ class LineBucket implements Bucket { }); } + const {lineAtlas, featureIndex} = options; + const hasFeatureDashes = this.addConstantDashes(lineAtlas); + for (const bucketFeature of bucketFeatures) { const {geometry, index, sourceLayerIndex} = bucketFeature; + if (hasFeatureDashes) { + this.addFeatureDashes(bucketFeature, lineAtlas); + } + if (this.hasPattern) { const patternBucketFeature = addPatternDependencies('line', this.layers, bucketFeature, this.zoom, options); // pattern features are added only once the pattern is loaded into the image atlas // so are stored during populate until later updated with positions by tile worker in addFeatures this.patternFeatures.push(patternBucketFeature); + } else { - this.addFeature(bucketFeature, geometry, index, canonical, {}); + this.addFeature(bucketFeature, geometry, index, canonical, lineAtlas.positions); } const feature = features[index].feature; - options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); + featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); + } + } + + addConstantDashes(lineAtlas: LineAtlas) { + let hasFeatureDashes = false; + + for (const layer of this.layers) { + const dashPropertyValue = layer.paint.get('line-dasharray').value; + const capPropertyValue = layer.layout.get('line-cap').value; + + if (dashPropertyValue.kind !== 'constant' || capPropertyValue.kind !== 'constant') { + hasFeatureDashes = true; + + } else { + const round = capPropertyValue.value === 'round'; + const constDash = dashPropertyValue.value; + if (!constDash) continue; + lineAtlas.addDash(constDash.from, round); + lineAtlas.addDash(constDash.to, round); + if (constDash.other) lineAtlas.addDash(constDash.other, round); + } + } + + return hasFeatureDashes; + } + + addFeatureDashes(feature: BucketFeature, lineAtlas: LineAtlas) { + + const zoom = this.zoom; + + for (const layer of this.layers) { + const dashPropertyValue = layer.paint.get('line-dasharray').value; + const capPropertyValue = layer.layout.get('line-cap').value; + + if (dashPropertyValue.kind === 'constant' && capPropertyValue.kind === 'constant') continue; + + let minDashArray, midDashArray, maxDashArray, minRound, midRound, maxRound; + + if (dashPropertyValue.kind === 'constant') { + const constDash = dashPropertyValue.value; + if (!constDash) continue; + minDashArray = constDash.other || constDash.to; + midDashArray = constDash.to; + maxDashArray = constDash.from; + + } else { + minDashArray = dashPropertyValue.evaluate({zoom: zoom - 1}, feature); + midDashArray = dashPropertyValue.evaluate({zoom}, feature); + maxDashArray = dashPropertyValue.evaluate({zoom: zoom + 1}, feature); + } + + if (capPropertyValue.kind === 'constant') { + minRound = midRound = maxRound = capPropertyValue.value === 'round'; + + } else { + minRound = capPropertyValue.evaluate({zoom: zoom - 1}, feature) === 'round'; + midRound = capPropertyValue.evaluate({zoom}, feature) === 'round'; + maxRound = capPropertyValue.evaluate({zoom: zoom + 1}, feature) === 'round'; + } + + lineAtlas.addDash(minDashArray, minRound); + lineAtlas.addDash(midDashArray, midRound); + lineAtlas.addDash(maxDashArray, maxRound); + + const min = lineAtlas.getKey(minDashArray, minRound); + const mid = lineAtlas.getKey(midDashArray, midRound); + const max = lineAtlas.getKey(maxDashArray, maxRound); + + // save positions for paint array + feature.patterns[layer.id] = {min, mid, max}; } + } update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { @@ -236,7 +316,7 @@ class LineBucket implements Bucket { addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { const layout = this.layers[0].layout; const join = layout.get('line-join').evaluate(feature, {}); - const cap = layout.get('line-cap'); + const cap = layout.get('line-cap').evaluate(feature, {}); const miterLimit = layout.get('line-miter-limit'); const roundLimit = layout.get('line-round-limit'); this.lineClips = this.lineFeatureClips(feature); diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index 30638af16d1..dc8c0602ee2 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -5,9 +5,10 @@ import Color from '../style-spec/util/color.js'; import {supportsPropertyExpression} from '../style-spec/util/properties.js'; import {register} from '../util/web_worker_transfer.js'; import {PossiblyEvaluatedPropertyValue} from '../style/properties.js'; -import {StructArrayLayout1f4, StructArrayLayout2f8, StructArrayLayout4f16, PatternLayoutArray} from './array_types.js'; +import {StructArrayLayout1f4, StructArrayLayout2f8, StructArrayLayout4f16, PatternLayoutArray, DashLayoutArray} from './array_types.js'; import {clamp} from '../util/util.js'; import patternAttributes from './bucket/pattern_attributes.js'; +import dashAttributes from './bucket/dash_attributes.js'; import EvaluationParameters from '../style/evaluation_parameters.js'; import FeaturePositionMap from './feature_position_map.js'; import { @@ -131,21 +132,21 @@ class CrossFadedConstantBinder implements UniformBinder { setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) { this.pixelRatioFrom = posFrom.pixelRatio; this.pixelRatioTo = posTo.pixelRatio; - this.patternFrom = posFrom.tlbr; - this.patternTo = posTo.tlbr; + this.patternFrom = posFrom.tl.concat(posFrom.br); + this.patternTo = posTo.tl.concat(posTo.br); } setUniform(uniform: Uniform<*>, globals: GlobalProperties, currentValue: PossiblyEvaluatedPropertyValue, uniformName: string) { const pos = - uniformName === 'u_pattern_to' ? this.patternTo : - uniformName === 'u_pattern_from' ? this.patternFrom : + uniformName === 'u_pattern_to' || uniformName === 'u_dash_to' ? this.patternTo : + uniformName === 'u_pattern_from' || uniformName === 'u_dash_from' ? this.patternFrom : uniformName === 'u_pixel_ratio_to' ? this.pixelRatioTo : uniformName === 'u_pixel_ratio_from' ? this.pixelRatioFrom : null; if (pos) uniform.set(pos); } getBinding(context: Context, location: WebGLUniformLocation, name: string): $Shape> { - return name.substr(0, 9) === 'u_pattern' ? + return name === 'u_pattern_from' || name === 'u_pattern_to' || name === 'u_dash_from' || name === 'u_dash_to' ? new Uniform4f(context, location) : new Uniform1f(context, location); } @@ -320,8 +321,9 @@ class CrossFadedCompositeBinder implements AttributeBinder { this.zoom = zoom; this.layerId = layerId; + this.paintVertexAttributes = (type === 'array' ? dashAttributes : patternAttributes).members; for (let i = 0; i < names.length; ++i) { - assert(`a_${names[i]}` === patternAttributes.members[i].name); + assert(`a_${names[i]}` === this.paintVertexAttributes[i].name); } this.zoomInPaintVertexArray = new PaintVertexArray(); @@ -352,25 +354,23 @@ class CrossFadedCompositeBinder implements AttributeBinder { // we're cross-fading to at layout time. In order to keep vertex attributes to a minimum and not pass // unnecessary vertex data to the shaders, we determine which to upload at draw time. for (let i = start; i < end; i++) { - this.zoomInPaintVertexArray.emplace(i, - imageMid.tl[0], imageMid.tl[1], imageMid.br[0], imageMid.br[1], - imageMin.tl[0], imageMin.tl[1], imageMin.br[0], imageMin.br[1], - imageMid.pixelRatio, - imageMin.pixelRatio, - ); - this.zoomOutPaintVertexArray.emplace(i, - imageMid.tl[0], imageMid.tl[1], imageMid.br[0], imageMid.br[1], - imageMax.tl[0], imageMax.tl[1], imageMax.br[0], imageMax.br[1], - imageMid.pixelRatio, - imageMax.pixelRatio, - ); + this._setPaintValue(this.zoomInPaintVertexArray, i, imageMid, imageMin); + this._setPaintValue(this.zoomOutPaintVertexArray, i, imageMid, imageMax); } } + _setPaintValue(array, i, posA, posB) { + array.emplace(i, + posA.tl[0], posA.tl[1], posA.br[0], posA.br[1], + posB.tl[0], posB.tl[1], posB.br[0], posB.br[1], + posA.pixelRatio, posB.pixelRatio + ); + } + upload(context: Context) { if (this.zoomInPaintVertexArray && this.zoomInPaintVertexArray.arrayBuffer && this.zoomOutPaintVertexArray && this.zoomOutPaintVertexArray.arrayBuffer) { - this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, patternAttributes.members, this.expression.isStateDependent); - this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, patternAttributes.members, this.expression.isStateDependent); + this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent); + this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent); } } @@ -425,13 +425,15 @@ export default class ProgramConfiguration { const propType = value.property.specification['property-type']; const isCrossFaded = propType === 'cross-faded' || propType === 'cross-faded-data-driven'; - if (expression.kind === 'constant') { + const sourceException = String(property) === 'line-dasharray' && (layer.layout: any).get('line-cap').value.kind !== 'constant'; + + if (expression.kind === 'constant' && !sourceException) { this.binders[property] = isCrossFaded ? new CrossFadedConstantBinder(expression.value, names) : new ConstantBinder(expression.value, names, type); keys.push(`/u_${property}`); - } else if (expression.kind === 'source' || isCrossFaded) { + } else if (expression.kind === 'source' || sourceException || isCrossFaded) { const StructArrayLayout = layoutType(property, type, 'source'); this.binders[property] = isCrossFaded ? new CrossFadedCompositeBinder(expression, names, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) : @@ -507,14 +509,10 @@ export default class ProgramConfiguration { const result = []; for (const property in this.binders) { const binder = this.binders[property]; - if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder) { + if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) { for (let i = 0; i < binder.paintVertexAttributes.length; i++) { result.push(binder.paintVertexAttributes[i].name); } - } else if (binder instanceof CrossFadedCompositeBinder) { - for (let i = 0; i < patternAttributes.members.length; i++) { - result.push(patternAttributes.members[i].name); - } } } return result; @@ -648,59 +646,60 @@ export class ProgramConfigurationSet { } } -function paintAttributeNames(property, type) { - const attributeNameExceptions = { - 'text-opacity': ['opacity'], - 'icon-opacity': ['opacity'], - 'text-color': ['fill_color'], - 'icon-color': ['fill_color'], - 'text-halo-color': ['halo_color'], - 'icon-halo-color': ['halo_color'], - 'text-halo-blur': ['halo_blur'], - 'icon-halo-blur': ['halo_blur'], - 'text-halo-width': ['halo_width'], - 'icon-halo-width': ['halo_width'], - 'line-gap-width': ['gapwidth'], - 'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], - 'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], - 'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], - }; +const attributeNameExceptions = { + 'text-opacity': ['opacity'], + 'icon-opacity': ['opacity'], + 'text-color': ['fill_color'], + 'icon-color': ['fill_color'], + 'text-halo-color': ['halo_color'], + 'icon-halo-color': ['halo_color'], + 'text-halo-blur': ['halo_blur'], + 'icon-halo-blur': ['halo_blur'], + 'text-halo-width': ['halo_width'], + 'icon-halo-width': ['halo_width'], + 'line-gap-width': ['gapwidth'], + 'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], + 'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], + 'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'], + 'line-dasharray': ['dash_to', 'dash_from'] +}; +function paintAttributeNames(property, type) { return attributeNameExceptions[property] || [property.replace(`${type}-`, '').replace(/-/g, '_')]; } -function getLayoutException(property) { - const propertyExceptions = { - 'line-pattern':{ - 'source': PatternLayoutArray, - 'composite': PatternLayoutArray - }, - 'fill-pattern': { - 'source': PatternLayoutArray, - 'composite': PatternLayoutArray - }, - 'fill-extrusion-pattern':{ - 'source': PatternLayoutArray, - 'composite': PatternLayoutArray - } - }; +const propertyExceptions = { + 'line-pattern': { + 'source': PatternLayoutArray, + 'composite': PatternLayoutArray + }, + 'fill-pattern': { + 'source': PatternLayoutArray, + 'composite': PatternLayoutArray + }, + 'fill-extrusion-pattern':{ + 'source': PatternLayoutArray, + 'composite': PatternLayoutArray + }, + 'line-dasharray': { // temporary layout + 'source': DashLayoutArray, + 'composite': DashLayoutArray + } +}; - return propertyExceptions[property]; -} +const defaultLayouts = { + 'color': { + 'source': StructArrayLayout2f8, + 'composite': StructArrayLayout4f16 + }, + 'number': { + 'source': StructArrayLayout1f4, + 'composite': StructArrayLayout2f8 + } +}; function layoutType(property, type, binderType) { - const defaultLayouts = { - 'color': { - 'source': StructArrayLayout2f8, - 'composite': StructArrayLayout4f16 - }, - 'number': { - 'source': StructArrayLayout1f4, - 'composite': StructArrayLayout2f8 - } - }; - - const layoutException = getLayoutException(property); + const layoutException = propertyExceptions[property]; return layoutException && layoutException[binderType] || defaultLayouts[type][binderType]; } diff --git a/src/render/draw_line.js b/src/render/draw_line.js index 9a605ae4417..a8a95a52256 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -29,7 +29,9 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly); const colorMode = painter.colorModeForRenderPass(); - const dasharray = layer.paint.get('line-dasharray'); + const dasharrayProperty = layer.paint.get('line-dasharray'); + const dasharray = dasharrayProperty.constantOr((1: any)); + const capProperty = layer.layout.get('line-cap'); const patternProperty = layer.paint.get('line-pattern'); const image = patternProperty.constantOr((1: any)); @@ -44,8 +46,6 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay const context = painter.context; const gl = context.gl; - let firstTile = true; - for (const coord of coords) { const tile = sourceCache.getTile(coord); if (image && !tile.patternsLoaded()) continue; @@ -55,9 +55,7 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay painter.prepareDrawTile(coord); const programConfiguration = bucket.programConfigurations.get(layer.id); - const prevProgram = painter.context.program.get(); const program = painter.useProgram(programId, programConfiguration); - const programChanged = firstTile || program.program !== prevProgram; const constantPattern = patternProperty.constantOr(null); if (constantPattern && tile.imageAtlas) { @@ -67,9 +65,20 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); } + const constantDash = dasharrayProperty.constantOr(null); + const constantCap = capProperty.constantOr((null: any)); + + if (!image && constantDash && constantCap && tile.lineAtlas) { + const atlas = tile.lineAtlas; + const round = constantCap === 'round'; + const posTo = atlas.getDash(constantDash.to, round); + const posFrom = atlas.getDash(constantDash.from, round); + if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); + } + const matrix = painter.terrain ? coord.posMatrix : null; const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade, matrix) : - dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade, matrix) : + dasharray ? lineSDFUniformValues(painter, tile, layer, crossfade, matrix) : gradient ? lineGradientUniformValues(painter, tile, layer, matrix, bucket.lineClipsArray.length) : lineUniformValues(painter, tile, layer, matrix); @@ -77,9 +86,10 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay context.activeTexture.set(gl.TEXTURE0); tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); programConfiguration.updatePaintBuffers(crossfade); - } else if (dasharray && (programChanged || painter.lineAtlas.dirty)) { + } else if (dasharray) { context.activeTexture.set(gl.TEXTURE0); - painter.lineAtlas.bind(context); + tile.lineAtlasTexture.bind(gl.LINEAR, gl.REPEAT); + programConfiguration.updatePaintBuffers(crossfade); } else if (gradient) { const layerGradient = bucket.gradients[layer.id]; let gradientTexture = layerGradient.texture; @@ -119,8 +129,5 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments, layer.paint, painter.transform.zoom, programConfiguration, bucket.layoutVertexBuffer2); - - firstTile = false; - // once refactored so that bound texture state is managed, we'll also be able to remove this firstTile/programChanged logic } } diff --git a/src/render/image_atlas.js b/src/render/image_atlas.js index e1e2165a385..1ac51570034 100644 --- a/src/render/image_atlas.js +++ b/src/render/image_atlas.js @@ -49,10 +49,6 @@ export class ImagePosition { ]; } - get tlbr(): Array { - return this.tl.concat(this.br); - } - get displaySize(): [number, number] { return [ (this.paddedRect.w - IMAGE_PADDING * 2) / this.pixelRatio, diff --git a/src/render/line_atlas.js b/src/render/line_atlas.js index c869141d587..f8d0fb620e1 100644 --- a/src/render/line_atlas.js +++ b/src/render/line_atlas.js @@ -1,8 +1,8 @@ // @flow -import {warnOnce} from '../util/util.js'; - -import type Context from '../gl/context.js'; +import {warnOnce, nextPowerOfTwo} from '../util/util.js'; +import {AlphaImage} from '../util/image.js'; +import {register} from '../util/web_worker_transfer.js'; /** * A LineAtlas lets us reuse rendered dashed lines @@ -17,24 +17,21 @@ class LineAtlas { width: number; height: number; nextRow: number; - bytes: number; - data: Uint8Array; - dashEntry: {[_: string]: any}; - dirty: boolean; - texture: WebGLTexture; + image: AlphaImage; + positions: {[_: string]: any}; + uploaded: boolean; constructor(width: number, height: number) { this.width = width; this.height = height; this.nextRow = 0; - - this.data = new Uint8Array(this.width * this.height); - - this.dashEntry = {}; + this.image = new AlphaImage({width, height}); + this.positions = {}; + this.uploaded = false; } /** - * Get or create a dash line pattern. + * Get a dash line pattern. * * @param {Array} dasharray * @param {boolean} round whether to add circle caps in between dash segments @@ -42,12 +39,18 @@ class LineAtlas { * @private */ getDash(dasharray: Array, round: boolean) { - const key = dasharray.join(",") + String(round); + const key = this.getKey(dasharray, round); + return this.positions[key]; + } - if (!this.dashEntry[key]) { - this.dashEntry[key] = this.addDash(dasharray, round); - } - return this.dashEntry[key]; + trim() { + const width = this.width; + const height = this.height = nextPowerOfTwo(this.nextRow); + this.image.resize({width, height}); + } + + getKey(dasharray: Array, round: boolean): string { + return dasharray.join(',') + String(round); } getDashRanges(dasharray: Array, lineAtlasWidth: number, stretch: number) { @@ -103,7 +106,7 @@ class LineAtlas { signedDistance = halfStretch - Math.sqrt(minDist * minDist + distMiddle * distMiddle); } - this.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128)); + this.image.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128)); } } } @@ -146,11 +149,12 @@ class LineAtlas { const minDist = Math.min(distLeft, distRight); const signedDistance = range.isDash ? minDist : -minDist; - this.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128)); + this.image.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128)); } } addDash(dasharray: Array, round: boolean) { + const key = this.getKey(dasharray, round); const n = round ? 7 : 0; const height = 2 * n + 1; @@ -185,38 +189,19 @@ class LineAtlas { } } - const dashEntry = { - y: (this.nextRow + n + 0.5) / this.height, - height: 2 * n / this.height, - width: length - }; + const y = this.nextRow + n; this.nextRow += height; - this.dirty = true; - - return dashEntry; - } - bind(context: Context) { - const gl = context.gl; - if (!this.texture) { - this.texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this.texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.width, this.height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.data); - - } else { - gl.bindTexture(gl.TEXTURE_2D, this.texture); - - if (this.dirty) { - this.dirty = false; - gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, this.height, gl.ALPHA, gl.UNSIGNED_BYTE, this.data); - } - } + const pos = { + tl: [y, n], + br: [length, 0] + }; + this.positions[key] = pos; + return pos; } } +register('LineAtlas', LineAtlas); + export default LineAtlas; diff --git a/src/render/program/line_program.js b/src/render/program/line_program.js index 12163a675af..caa9fe3dbad 100644 --- a/src/render/program/line_program.js +++ b/src/render/program/line_program.js @@ -15,7 +15,6 @@ import type Context from '../../gl/context.js'; import type {UniformValues, UniformLocations} from '../uniform_binding.js'; import type Transform from '../../geo/transform.js'; import type Tile from '../../source/tile.js'; -import type {CrossFaded} from '../../style/properties.js'; import type LineStyleLayer from '../../style/style_layer/line_style_layer.js'; import type Painter from '../painter.js'; import type {CrossfadeParameters} from '../../style/evaluation_parameters.js'; @@ -49,15 +48,12 @@ export type LinePatternUniformsType = {| export type LineSDFUniformsType = {| 'u_matrix': UniformMatrix4f, + 'u_texsize': Uniform2f, 'u_ratio': Uniform1f, 'u_device_pixel_ratio': Uniform1f, 'u_units_to_pixels': Uniform2f, - 'u_patternscale_a': Uniform2f, - 'u_patternscale_b': Uniform2f, - 'u_sdfgamma': Uniform1f, + 'u_scale': Uniform3f, 'u_image': Uniform1i, - 'u_tex_y_a': Uniform1f, - 'u_tex_y_b': Uniform1f, 'u_mix': Uniform1f |}; @@ -90,15 +86,12 @@ const linePatternUniforms = (context: Context, locations: UniformLocations): Lin const lineSDFUniforms = (context: Context, locations: UniformLocations): LineSDFUniformsType => ({ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), + 'u_texsize': new Uniform2f(context, locations.u_texsize), 'u_ratio': new Uniform1f(context, locations.u_ratio), 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), 'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels), - 'u_patternscale_a': new Uniform2f(context, locations.u_patternscale_a), - 'u_patternscale_b': new Uniform2f(context, locations.u_patternscale_b), - 'u_sdfgamma': new Uniform1f(context, locations.u_sdfgamma), 'u_image': new Uniform1i(context, locations.u_image), - 'u_tex_y_a': new Uniform1f(context, locations.u_tex_y_a), - 'u_tex_y_b': new Uniform1f(context, locations.u_tex_y_b), + 'u_scale': new Uniform3f(context, locations.u_scale), 'u_mix': new Uniform1f(context, locations.u_mix) }); @@ -163,29 +156,14 @@ const lineSDFUniformValues = ( painter: Painter, tile: Tile, layer: LineStyleLayer, - dasharray: CrossFaded>, crossfade: CrossfadeParameters, matrix: ?Float32Array ): UniformValues => { - const transform = painter.transform; - const lineAtlas = painter.lineAtlas; - const tileRatio = calculateTileRatio(tile, transform); - - const round = layer.layout.get('line-cap') === 'round'; - - const posA = lineAtlas.getDash(dasharray.from, round); - const posB = lineAtlas.getDash(dasharray.to, round); - - const widthA = posA.width * crossfade.fromScale; - const widthB = posB.width * crossfade.toScale; - + const tileZoomRatio = calculateTileRatio(tile, painter.transform); return extend(lineUniformValues(painter, tile, layer, matrix), { - 'u_patternscale_a': [tileRatio / widthA, -posA.height / 2], - 'u_patternscale_b': [tileRatio / widthB, -posB.height / 2], - 'u_sdfgamma': lineAtlas.width / (Math.min(widthA, widthB) * 256 * browser.devicePixelRatio) / 2, + 'u_texsize': tile.lineAtlasTexture.size, + 'u_scale': [tileZoomRatio, crossfade.fromScale, crossfade.toScale], 'u_image': 0, - 'u_tex_y_a': posA.y, - 'u_tex_y_b': posB.y, 'u_mix': crossfade.t }); }; diff --git a/src/shaders/line_sdf.fragment.glsl b/src/shaders/line_sdf.fragment.glsl index 70a74996b34..6abbeb3a562 100644 --- a/src/shaders/line_sdf.fragment.glsl +++ b/src/shaders/line_sdf.fragment.glsl @@ -1,8 +1,8 @@ uniform lowp float u_device_pixel_ratio; uniform sampler2D u_image; -uniform float u_sdfgamma; uniform float u_mix; +uniform vec3 u_scale; varying vec2 v_normal; varying vec2 v_width2; @@ -15,6 +15,8 @@ varying float v_gamma_scale; #pragma mapbox: define lowp float opacity #pragma mapbox: define mediump float width #pragma mapbox: define lowp float floorwidth +#pragma mapbox: define lowp vec4 dash_from +#pragma mapbox: define lowp vec4 dash_to void main() { #pragma mapbox: initialize highp vec4 color @@ -22,6 +24,8 @@ void main() { #pragma mapbox: initialize lowp float opacity #pragma mapbox: initialize mediump float width #pragma mapbox: initialize lowp float floorwidth + #pragma mapbox: initialize mediump vec4 dash_from + #pragma mapbox: initialize mediump vec4 dash_to // Calculate the distance of the pixel from the line in pixels. float dist = length(v_normal) * v_width2.s; @@ -35,7 +39,9 @@ void main() { float sdfdist_a = texture2D(u_image, v_tex_a).a; float sdfdist_b = texture2D(u_image, v_tex_b).a; float sdfdist = mix(sdfdist_a, sdfdist_b, u_mix); - alpha *= smoothstep(0.5 - u_sdfgamma / floorwidth, 0.5 + u_sdfgamma / floorwidth, sdfdist); + float sdfwidth = min(dash_from.z * u_scale.y, dash_to.z * u_scale.z); + float sdfgamma = 1.0 / (2.0 * u_device_pixel_ratio) / sdfwidth; + alpha *= smoothstep(0.5 - sdfgamma / floorwidth, 0.5 + sdfgamma / floorwidth, sdfdist); gl_FragColor = color * (alpha * opacity); diff --git a/src/shaders/line_sdf.vertex.glsl b/src/shaders/line_sdf.vertex.glsl index 7dc1d739c00..941c8111e4a 100644 --- a/src/shaders/line_sdf.vertex.glsl +++ b/src/shaders/line_sdf.vertex.glsl @@ -4,7 +4,7 @@ // there are also "special" normals that have a bigger length (of up to 126 in // this case). // #define scale 63.0 -#define scale 0.015873016 +#define EXTRUDE_SCALE 0.015873016 attribute vec2 a_pos_normal; attribute vec4 a_data; @@ -13,12 +13,11 @@ attribute float a_linesofar; uniform mat4 u_matrix; uniform mediump float u_ratio; uniform lowp float u_device_pixel_ratio; -uniform vec2 u_patternscale_a; -uniform float u_tex_y_a; -uniform vec2 u_patternscale_b; -uniform float u_tex_y_b; uniform vec2 u_units_to_pixels; +uniform vec2 u_texsize; +uniform mediump vec3 u_scale; + varying vec2 v_normal; varying vec2 v_width2; varying vec2 v_tex_a; @@ -32,6 +31,8 @@ varying float v_gamma_scale; #pragma mapbox: define lowp float offset #pragma mapbox: define mediump float width #pragma mapbox: define lowp float floorwidth +#pragma mapbox: define lowp vec4 dash_from +#pragma mapbox: define lowp vec4 dash_to void main() { #pragma mapbox: initialize highp vec4 color @@ -41,6 +42,8 @@ void main() { #pragma mapbox: initialize lowp float offset #pragma mapbox: initialize mediump float width #pragma mapbox: initialize lowp float floorwidth + #pragma mapbox: initialize mediump vec4 dash_from + #pragma mapbox: initialize mediump vec4 dash_to // the distance over which the line edge fades out. // Retina devices need a smaller distance to avoid aliasing. @@ -69,7 +72,7 @@ void main() { // Scale the extrusion vector down to a normal and then up by the line width // of this vertex. - mediump vec2 dist = outset * a_extrude * scale; + mediump vec2 dist = outset * a_extrude * EXTRUDE_SCALE; // Calculate the offset when drawing a line that is to the side of the actual line. // We do this by creating a vector that points towards the extrude, but rotate @@ -77,7 +80,7 @@ void main() { // extrude vector points in another direction. mediump float u = 0.5 * a_direction; mediump float t = 1.0 - abs(u); - mediump vec2 offset2 = offset * a_extrude * scale * normal.y * mat2(t, -u, u, t); + mediump vec2 offset2 = offset * a_extrude * EXTRUDE_SCALE * normal.y * mat2(t, -u, u, t); vec4 projected_extrude = u_matrix * vec4(dist / u_ratio, 0.0, 0.0); gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude; @@ -91,8 +94,17 @@ void main() { v_gamma_scale = 1.0; #endif - v_tex_a = vec2(a_linesofar * u_patternscale_a.x / floorwidth, normal.y * u_patternscale_a.y + u_tex_y_a); - v_tex_b = vec2(a_linesofar * u_patternscale_b.x / floorwidth, normal.y * u_patternscale_b.y + u_tex_y_b); + float tileZoomRatio = u_scale.x; + float fromScale = u_scale.y; + float toScale = u_scale.z; + + float widthA = dash_from.z * fromScale; + float widthB = dash_to.z * toScale; + float heightA = dash_from.y; + float heightB = dash_to.y; + + v_tex_a = vec2(a_linesofar * (tileZoomRatio / widthA) / floorwidth, (-normal.y * heightA + dash_from.x + 0.5) / u_texsize.y); + v_tex_b = vec2(a_linesofar * (tileZoomRatio / widthB) / floorwidth, (-normal.y * heightB + dash_to.x + 0.5) / u_texsize.y); v_width2 = vec2(outset, inset); } diff --git a/src/source/tile.js b/src/source/tile.js index eb36fb2e23a..a29dc1629ee 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -26,6 +26,7 @@ import type Actor from '../util/actor.js'; import type DEMData from '../data/dem_data.js'; import type {AlphaImage} from '../util/image.js'; import type ImageAtlas from '../render/image_atlas.js'; +import type LineAtlas from '../render/line_atlas.js'; import type ImageManager from '../render/image_manager.js'; import type Context from '../gl/context.js'; import type {OverscaledTileID} from './tile_id.js'; @@ -62,6 +63,8 @@ class Tile { latestRawTileData: ?ArrayBuffer; imageAtlas: ?ImageAtlas; imageAtlasTexture: Texture; + lineAtlas: ?LineAtlas; + lineAtlasTexture: Texture; glyphAtlasImage: ?AlphaImage; glyphAtlasTexture: Texture; expirationTime: any; @@ -215,6 +218,9 @@ class Tile { if (data.glyphAtlasImage) { this.glyphAtlasImage = data.glyphAtlasImage; } + if (data.lineAtlas) { + this.lineAtlas = data.lineAtlas; + } } /** @@ -228,17 +234,26 @@ class Tile { } this.buckets = {}; - if (this.imageAtlasTexture) { - this.imageAtlasTexture.destroy(); - } - if (this.imageAtlas) { this.imageAtlas = null; } + if (this.lineAtlas) { + this.lineAtlas = null; + } + + if (this.imageAtlasTexture) { + this.imageAtlasTexture.destroy(); + } + if (this.glyphAtlasTexture) { this.glyphAtlasTexture.destroy(); } + + if (this.lineAtlasTexture) { + this.lineAtlasTexture.destroy(); + } + Debug.run(() => { if (this.queryGeometryDebugViz) { this.queryGeometryDebugViz.unload(); @@ -275,6 +290,11 @@ class Tile { this.glyphAtlasTexture = new Texture(context, this.glyphAtlasImage, gl.ALPHA); this.glyphAtlasImage = null; } + + if (this.lineAtlas && !this.lineAtlas.uploaded) { + this.lineAtlasTexture = new Texture(context, this.lineAtlas.image, gl.ALPHA); + this.lineAtlas.uploaded = true; + } } prepare(imageManager: ImageManager) { diff --git a/src/source/worker_source.js b/src/source/worker_source.js index d9c37b37630..9c33bf574db 100644 --- a/src/source/worker_source.js +++ b/src/source/worker_source.js @@ -4,6 +4,7 @@ import type {RequestParameters} from '../util/ajax.js'; import type {RGBAImage, AlphaImage} from '../util/image.js'; import type {GlyphPositions} from '../render/glyph_atlas.js'; import type ImageAtlas from '../render/image_atlas.js'; +import type LineAtlas from '../render/line_atlas.js'; import type {OverscaledTileID} from './tile_id.js'; import type {Bucket} from '../data/bucket.js'; import type FeatureIndex from '../data/feature_index.js'; @@ -52,6 +53,7 @@ export type WorkerTileResult = { buckets: Array, imageAtlas: ImageAtlas, glyphAtlasImage: AlphaImage, + lineAtlas: LineAtlas, featureIndex: FeatureIndex, collisionBoxArray: CollisionBoxArray, rawTileData?: ArrayBuffer, diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 9356cf11076..6aa2fd0312b 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -11,6 +11,7 @@ import FillBucket from '../data/bucket/fill_bucket.js'; import FillExtrusionBucket from '../data/bucket/fill_extrusion_bucket.js'; import {warnOnce, mapObject, values} from '../util/util.js'; import assert from 'assert'; +import LineAtlas from '../render/line_atlas.js'; import ImageAtlas from '../render/image_atlas.js'; import GlyphAtlas from '../render/glyph_atlas.js'; import EvaluationParameters from '../style/evaluation_parameters.js'; @@ -83,11 +84,15 @@ class WorkerTile { const buckets: {[_: string]: Bucket} = {}; + // we initially reserve space for a 256x256 atlas, but trim it after processing all line features + const lineAtlas = new LineAtlas(256, 256); + const options = { featureIndex, iconDependencies: {}, patternDependencies: {}, glyphDependencies: {}, + lineAtlas, availableImages }; @@ -155,6 +160,8 @@ class WorkerTile { } } + lineAtlas.trim(); + let error: ?Error; let glyphMap: ?{[_: string]: {[_: number]: ?StyleGlyph}}; let iconMap: ?{[_: string]: StyleImage}; @@ -239,6 +246,7 @@ class WorkerTile { featureIndex, collisionBoxArray: this.collisionBoxArray, glyphAtlasImage: glyphAtlas.image, + lineAtlas, imageAtlas, // Only used for benchmarking: glyphMap: this.returnDependencies ? glyphMap : null, diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index fa43a896a55..b31fbe23031 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -884,15 +884,19 @@ "android": "2.0.1", "ios": "2.0.0", "macos": "0.1.0" + }, + "data-driven styling": { + "js": "2.3.0" } }, "expression": { "interpolated": false, "parameters": [ - "zoom" + "zoom", + "feature" ] }, - "property-type": "data-constant" + "property-type": "data-driven" }, "line-join": { "type": "enum", @@ -4532,15 +4536,18 @@ "ios": "2.0.0", "macos": "0.1.0" }, - "data-driven styling": {} + "data-driven styling": { + "js": "2.3.0" + } }, "expression": { "interpolated": false, "parameters": [ - "zoom" + "zoom", + "feature" ] }, - "property-type": "cross-faded" + "property-type": "cross-faded-data-driven" }, "line-pattern": { "type": "resolvedImage", diff --git a/src/style-spec/types.js b/src/style-spec/types.js index 1186be1c05e..0269f8c7819 100644 --- a/src/style-spec/types.js +++ b/src/style-spec/types.js @@ -197,7 +197,7 @@ export type LineLayerSpecification = {| "maxzoom"?: number, "filter"?: FilterSpecification, "layout"?: {| - "line-cap"?: PropertyValueSpecification<"butt" | "round" | "square">, + "line-cap"?: DataDrivenPropertyValueSpecification<"butt" | "round" | "square">, "line-join"?: DataDrivenPropertyValueSpecification<"bevel" | "round" | "miter">, "line-miter-limit"?: PropertyValueSpecification, "line-round-limit"?: PropertyValueSpecification, @@ -213,7 +213,7 @@ export type LineLayerSpecification = {| "line-gap-width"?: DataDrivenPropertyValueSpecification, "line-offset"?: DataDrivenPropertyValueSpecification, "line-blur"?: DataDrivenPropertyValueSpecification, - "line-dasharray"?: PropertyValueSpecification>, + "line-dasharray"?: DataDrivenPropertyValueSpecification>, "line-pattern"?: DataDrivenPropertyValueSpecification, "line-gradient"?: ExpressionSpecification |} diff --git a/src/style/properties.js b/src/style/properties.js index 1ebf0e850d5..71084fe9de9 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -27,7 +27,8 @@ type TimePoint = number; export type CrossFaded = { to: T, - from: T + from: T, + other?: T }; /** @@ -634,7 +635,12 @@ export class CrossFadedDataDrivenProperty extends DataDrivenProperty { const z = parameters.zoom; - return z > parameters.zoomHistory.lastIntegerZoom ? {from: min, to: mid} : {from: max, to: mid}; + // ugly hack alert: when evaluating non-constant dashes on the worker side, + // we need all three values to pack into the atlas; the if condition is always false there; + // will be removed after removing cross-fading + return z > parameters.zoomHistory.lastIntegerZoom ? + {from: min, to: mid, other: max} : + {from: max, to: mid, other: min}; } interpolate(a: PossiblyEvaluatedPropertyValue>): PossiblyEvaluatedPropertyValue> { diff --git a/src/style/style_layer/line_style_layer_properties.js b/src/style/style_layer/line_style_layer_properties.js index d70e97d076e..729725d70ce 100644 --- a/src/style/style_layer/line_style_layer_properties.js +++ b/src/style/style_layer/line_style_layer_properties.js @@ -20,7 +20,7 @@ import type Formatted from '../../style-spec/expression/types/formatted.js'; import type ResolvedImage from '../../style-spec/expression/types/resolved_image.js'; export type LayoutProps = {| - "line-cap": DataConstantProperty<"butt" | "round" | "square">, + "line-cap": DataDrivenProperty<"butt" | "round" | "square">, "line-join": DataDrivenProperty<"bevel" | "round" | "miter">, "line-miter-limit": DataConstantProperty, "line-round-limit": DataConstantProperty, @@ -28,7 +28,7 @@ export type LayoutProps = {| |}; const layout: Properties = new Properties({ - "line-cap": new DataConstantProperty(styleSpec["layout_line"]["line-cap"]), + "line-cap": new DataDrivenProperty(styleSpec["layout_line"]["line-cap"]), "line-join": new DataDrivenProperty(styleSpec["layout_line"]["line-join"]), "line-miter-limit": new DataConstantProperty(styleSpec["layout_line"]["line-miter-limit"]), "line-round-limit": new DataConstantProperty(styleSpec["layout_line"]["line-round-limit"]), @@ -44,7 +44,7 @@ export type PaintProps = {| "line-gap-width": DataDrivenProperty, "line-offset": DataDrivenProperty, "line-blur": DataDrivenProperty, - "line-dasharray": CrossFadedProperty>, + "line-dasharray": CrossFadedDataDrivenProperty>, "line-pattern": CrossFadedDataDrivenProperty, "line-gradient": ColorRampProperty, |}; @@ -58,7 +58,7 @@ const paint: Properties = new Properties({ "line-gap-width": new DataDrivenProperty(styleSpec["paint_line"]["line-gap-width"]), "line-offset": new DataDrivenProperty(styleSpec["paint_line"]["line-offset"]), "line-blur": new DataDrivenProperty(styleSpec["paint_line"]["line-blur"]), - "line-dasharray": new CrossFadedProperty(styleSpec["paint_line"]["line-dasharray"]), + "line-dasharray": new CrossFadedDataDrivenProperty(styleSpec["paint_line"]["line-dasharray"]), "line-pattern": new CrossFadedDataDrivenProperty(styleSpec["paint_line"]["line-pattern"]), "line-gradient": new ColorRampProperty(styleSpec["paint_line"]["line-gradient"]), }); diff --git a/test/integration/render-tests/line-cap/data-driven/expected.png b/test/integration/render-tests/line-cap/data-driven/expected.png new file mode 100644 index 00000000000..5d1599bbf27 Binary files /dev/null and b/test/integration/render-tests/line-cap/data-driven/expected.png differ diff --git a/test/integration/render-tests/line-cap/data-driven/style.json b/test/integration/render-tests/line-cap/data-driven/style.json new file mode 100644 index 00000000000..8de29dc2c24 --- /dev/null +++ b/test/integration/render-tests/line-cap/data-driven/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 48 + } + }, + "zoom": 0, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"property": 2}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -10], [40, -10]] + } + }, + { + "type": "Feature", + "properties": {"property": 4}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 0], [40, 0]] + } + }, + { + "type": "Feature", + "properties": {"property": 6}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 10], [40, 10]] + } + } + ] + } + } + }, + "layers": [ + { + "id": "road", + "type": "line", + "source": "geojson", + "layout": { + "line-cap": [ + "match", ["get", "property"], + 2, "square", + 4, "butt", + "round" + ] + }, + "paint": { + "line-width": 6 + } + } + ] +} diff --git a/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/expected.png b/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/expected.png new file mode 100644 index 00000000000..ef16adf79d4 Binary files /dev/null and b/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/style.json b/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/style.json new file mode 100644 index 00000000000..bc95e002e7e --- /dev/null +++ b/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/style.json @@ -0,0 +1,89 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 64 + } + }, + "zoom": 0.1, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"property": 2}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -18], [40, -18]] + } + }, + { + "type": "Feature", + "properties": {"property": 3}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -10], [40, -10]] + } + }, + { + "type": "Feature", + "properties": {"property": 4}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -2], [40, -2]] + } + }, + { + "type": "Feature", + "properties": {"property": 5}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 7], [40, 7]] + } + }, + { + "type": "Feature", + "properties": {"property": 6}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 18], [40, 18]] + } + } + ] + } + } + }, + "layers": [ + { + "id": "road", + "type": "line", + "source": "geojson", + "layout": { + "line-cap": [ + "step", ["zoom"], + ["case", ["==", ["%", ["get", "property"], 2], 0], "butt", "round"], + 1, "butt" + ] + }, + "paint": { + "line-width": ["get", "property"], + "line-dasharray": [ + "step", ["zoom"], + [ + "match", ["get", "property"], + 2, ["literal", [2, 2]], + 3, ["literal", [2, 3]], + 4, ["literal", [2, 4]], + 5, ["literal", [2, 5]], + ["literal", [2, 6]] + ], + 1, ["literal", [1, 1]] + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/expected.png b/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/expected.png new file mode 100644 index 00000000000..53b0a629d3b Binary files /dev/null and b/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/style.json b/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/style.json new file mode 100644 index 00000000000..01db0779b87 --- /dev/null +++ b/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 64 + } + }, + "zoom": 0, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"property": 2}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -18], [40, -18]] + } + }, + { + "type": "Feature", + "properties": {"property": 3}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -10], [40, -10]] + } + }, + { + "type": "Feature", + "properties": {"property": 4}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -2], [40, -2]] + } + }, + { + "type": "Feature", + "properties": {"property": 5}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 7], [40, 7]] + } + }, + { + "type": "Feature", + "properties": {"property": 6}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 18], [40, 18]] + } + } + ] + } + } + }, + "layers": [ + { + "id": "road", + "type": "line", + "source": "geojson", + "layout": { + "line-cap": ["case", ["==", ["%", ["get", "property"], 2], 0], "butt", "round"] + }, + "paint": { + "line-width": ["get", "property"], + "line-dasharray": [2, 2] + } + } + ] +} diff --git a/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/expected.png b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/expected.png new file mode 100644 index 00000000000..7465f4abfab Binary files /dev/null and b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/style.json b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/style.json new file mode 100644 index 00000000000..41cfe23f681 --- /dev/null +++ b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/style.json @@ -0,0 +1,81 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 64 + } + }, + "zoom": 0, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"property": 2}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -18], [40, -18]] + } + }, + { + "type": "Feature", + "properties": {"property": 3}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -10], [40, -10]] + } + }, + { + "type": "Feature", + "properties": {"property": 4}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -2], [40, -2]] + } + }, + { + "type": "Feature", + "properties": {"property": 5}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 7], [40, 7]] + } + }, + { + "type": "Feature", + "properties": {"property": 6}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 18], [40, 18]] + } + } + ] + } + } + }, + "layers": [ + { + "id": "road", + "type": "line", + "source": "geojson", + "layout": { + "line-cap": "butt" + }, + "paint": { + "line-width": ["get", "property"], + "line-dasharray": [ + "match", ["get", "property"], + 2, ["literal", [2, 2]], + 3, ["literal", [2, 3]], + 4, ["literal", [2, 4]], + 5, ["literal", [2, 5]], + ["literal", [2, 6]] + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/expected.png b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/expected.png new file mode 100644 index 00000000000..093c5005351 Binary files /dev/null and b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/expected.png differ diff --git a/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/style.json b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/style.json new file mode 100644 index 00000000000..a356b05a3f1 --- /dev/null +++ b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/style.json @@ -0,0 +1,81 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 64 + } + }, + "zoom": 0, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"property": 2}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -18], [40, -18]] + } + }, + { + "type": "Feature", + "properties": {"property": 3}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -10], [40, -10]] + } + }, + { + "type": "Feature", + "properties": {"property": 4}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, -2], [40, -2]] + } + }, + { + "type": "Feature", + "properties": {"property": 5}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 7], [40, 7]] + } + }, + { + "type": "Feature", + "properties": {"property": 6}, + "geometry": { + "type": "LineString", + "coordinates": [[-40, 18], [40, 18]] + } + } + ] + } + } + }, + "layers": [ + { + "id": "road", + "type": "line", + "source": "geojson", + "layout": { + "line-cap": ["case", ["==", ["%", ["get", "property"], 2], 0], "butt", "round"] + }, + "paint": { + "line-width": ["get", "property"], + "line-dasharray": [ + "match", ["get", "property"], + 2, ["literal", [2, 2]], + 3, ["literal", [2, 3]], + 4, ["literal", [2, 4]], + 5, ["literal", [2, 5]], + ["literal", [2, 6]] + ] + } + } + ] +} diff --git a/test/unit/render/line_atlas.test.js b/test/unit/render/line_atlas.test.js index ef607b93ab6..1f2f2e4abb7 100644 --- a/test/unit/render/line_atlas.test.js +++ b/test/unit/render/line_atlas.test.js @@ -5,43 +5,43 @@ test('LineAtlas', (t) => { const lineAtlas = new LineAtlas(64, 64); t.test('round [0, 0]', (t) => { const entry = lineAtlas.addDash([0, 0], true); - t.equal(entry.width, 0); + t.equal(entry.br[0], 0); t.end(); }); t.test('round [1, 0]', (t) => { const entry = lineAtlas.addDash([1, 0], true); - t.equal(entry.width, 1); + t.equal(entry.br[0], 1); t.end(); }); t.test('round [0, 1]', (t) => { const entry = lineAtlas.addDash([0, 1], true); - t.equal(entry.width, 1); + t.equal(entry.br[0], 1); t.end(); }); t.test('odd round [1, 2, 1]', (t) => { const entry = lineAtlas.addDash([1, 2, 1], true); - t.equal(entry.width, 4); + t.equal(entry.br[0], 4); t.end(); }); t.test('regular [0, 0]', (t) => { const entry = lineAtlas.addDash([0, 0], false); - t.equal(entry.width, 0); + t.equal(entry.br[0], 0); t.end(); }); t.test('regular [1, 0]', (t) => { const entry = lineAtlas.addDash([1, 0], false); - t.equal(entry.width, 1); + t.equal(entry.br[0], 1); t.end(); }); t.test('regular [0, 1]', (t) => { const entry = lineAtlas.addDash([0, 1], false); - t.equal(entry.width, 1); + t.equal(entry.br[0], 1); t.end(); }); t.test('odd regular [1, 2, 1]', (t) => { const entry = lineAtlas.addDash([1, 2, 1], false); - t.equal(entry.width, 4); + t.equal(entry.br[0], 4); t.end(); }); t.end(); diff --git a/test/unit/style-spec/fixture/functions.output-api-supported.json b/test/unit/style-spec/fixture/functions.output-api-supported.json index 580f30573a8..9461099bd21 100644 --- a/test/unit/style-spec/fixture/functions.output-api-supported.json +++ b/test/unit/style-spec/fixture/functions.output-api-supported.json @@ -28,7 +28,7 @@ "line": 75 }, { - "message": "layers[6].paint.line-dasharray.stops[0][0]: number expected, string found", + "message": "layers[6].paint.line-dasharray.stops[0][0]: number expected, string found\nIf you intended to use a categorical function, specify `\"type\": \"categorical\"`.", "line": 89 }, { diff --git a/test/unit/style-spec/fixture/functions.output.json b/test/unit/style-spec/fixture/functions.output.json index 580f30573a8..9461099bd21 100644 --- a/test/unit/style-spec/fixture/functions.output.json +++ b/test/unit/style-spec/fixture/functions.output.json @@ -28,7 +28,7 @@ "line": 75 }, { - "message": "layers[6].paint.line-dasharray.stops[0][0]: number expected, string found", + "message": "layers[6].paint.line-dasharray.stops[0][0]: number expected, string found\nIf you intended to use a categorical function, specify `\"type\": \"categorical\"`.", "line": 89 }, {