From 291ba36642933856776417d58c8ec5326ff065a1 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Thu, 6 May 2021 12:48:52 +0300 Subject: [PATCH] Data-driven `line-dasharray` and `line-cap` (#10591) * WIP data-driven line-dasharray / line-cap (not working yet) * WIP pass the per-tile line atlas through * WIP switch to integer coords for line atlas * add a dds dasharray debug page (temp) * fix debug page * use different attribute names for dashes to avoid collision The attribute names used for dashes were the same as those used for patterns. This was leading to incorrectly generated shaders. Since there was no pattern set for the dash layer it was setting the defines that generate the shader without those attributes. The line atlas texture is now bound with gl.REPEAT to render more than one dash. * fix DDS dasharray rendering * refactor to support constant patterns * fix constant dasharrays * dasharray fixes * more dasharray fixes * fix line atlas unit tests * more unit test fixes * fix remaining render test * one more unit test fix * optimize dash buffer layout * fixup line pattern * fix constant dash + dds line-cap * fixup * fix dasharray + composite line-cap * fix dasharray flickering when crossing zoom stops * dasharray perf optimizations * clean up leftovers * minor optimization in program_configuration * add data-driven dash/cap render tests * add a render test for solid line data-driven line-cap * make shaders more consistent for easier porting * rename conflicting shader constant * fix out of atlas space warning in some cases Co-authored-by: Ansis Brammanis --- build/generate-struct-arrays.js | 4 +- debug/dasharray.html | 72 +++++++++ src/data/array_types.js | 40 +++++ src/data/bucket.js | 4 +- src/data/bucket/dash_attributes.js | 9 ++ src/data/bucket/line_bucket.js | 86 +++++++++- src/data/program_configuration.js | 147 +++++++++--------- src/render/draw_line.js | 29 ++-- src/render/image_atlas.js | 4 - src/render/line_atlas.js | 81 ++++------ src/render/program/line_program.js | 36 +---- src/shaders/line_sdf.fragment.glsl | 10 +- src/shaders/line_sdf.vertex.glsl | 30 ++-- src/source/tile.js | 28 +++- src/source/worker_source.js | 2 + src/source/worker_tile.js | 8 + src/style-spec/reference/v8.json | 17 +- src/style-spec/types.js | 4 +- src/style/properties.js | 10 +- .../line_style_layer_properties.js | 8 +- .../line-cap/data-driven/expected.png | Bin 0 -> 452 bytes .../line-cap/data-driven/style.json | 62 ++++++++ .../composite-dash-composite-cap/expected.png | Bin 0 -> 1805 bytes .../composite-dash-composite-cap/style.json | 89 +++++++++++ .../const-dash-feature-cap/expected.png | Bin 0 -> 951 bytes .../const-dash-feature-cap/style.json | 74 +++++++++ .../feature-dash-const-cap/expected.png | Bin 0 -> 831 bytes .../feature-dash-const-cap/style.json | 81 ++++++++++ .../feature-dash-feature-cap/expected.png | Bin 0 -> 924 bytes .../feature-dash-feature-cap/style.json | 81 ++++++++++ test/unit/render/line_atlas.test.js | 16 +- .../functions.output-api-supported.json | 2 +- .../style-spec/fixture/functions.output.json | 2 +- 33 files changed, 827 insertions(+), 209 deletions(-) create mode 100644 debug/dasharray.html create mode 100644 src/data/bucket/dash_attributes.js create mode 100644 test/integration/render-tests/line-cap/data-driven/expected.png create mode 100644 test/integration/render-tests/line-cap/data-driven/style.json create mode 100644 test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/expected.png create mode 100644 test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/style.json create mode 100644 test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/expected.png create mode 100644 test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/style.json create mode 100644 test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/expected.png create mode 100644 test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/style.json create mode 100644 test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/expected.png create mode 100644 test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/style.json 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 0000000000000000000000000000000000000000..5d1599bbf279bf986847a5de6e0f1991cac795cc GIT binary patch literal 452 zcmV;#0XzPQP)2w>iw#YzIO7~?zy0nEFzSV;gD zW1NQ|fO&ToD+$12jPno#Fz?P{WiNma3rZkX=S%<@aS%YpoL4CcfBu|>FHC|RWpmAskXo@qx~1a!cL2<1N`LV z8O883fqBk_t_Dtb@{D5mV!)@)h5i9n0rYPrSpfV2EaF^fS>Q(}&nSjp00;ygx(qnk z!!V+;0>E$1wd8nxm;(R~!vz3h2Z$XY9s!OmV?;ya5zxZWaRz8{23WC$p})-?G+NBL zAXt_JmT*p_gVxVZo>2{N?FO&@+d6)d>K2E!MN~0f3%4)T>Sa zz-zIdjSm3y%%NU&0svl%^=y0qpl1&CsuKY4TC8W|0{}g9s8^j70BZwl0Zl;h7w`%2 z(U8Nyx@({i=$Z```w&oQdYJbUlw)6GAk#t((V(S6|#Db=ifJ<9WdJJ%FOWry*TWtXR-hwyBB7Jq6$wRjS za|M8df!jQsX6zBb%^mowSpRHsJ*}AQ7htI&$kYXt66d%Uj%uQYl2}R^d+87X;8fre zFK06Tq9NR)Qx;E7>_WQNu%LIQtAJxW2sMlwM~imYRpto*TLQ0kP--^tS_l3r)(;v! z&&v4U0yZClOgjM2c9?e?;DrwSRjls<8@8Btc5w#S2RH#(!7+&rG&c@uOOqIq<@&*a z%OTc(2UG28?|YaWV~+<82bvrJi)m%6n*i+~!=PmbzG!+_)gb>ksf8uzyF{Hvu$H9i0f=-f^8XfKxJaNS5z9uA|WIdNp%=4Q$qN z4JCS%>aHUtfhi1K>vK|(kKAn zGFSfw2LSr#Q>-S=F{FaE_#_*IRg@BDRff5i>-UAwHHMD+!qXJR1Y zQ%pc|SW+xN{B>QsR0|;+mfg%&tU~hZr;3y>5PwySMSY;><1YZP6Yx|9-*Vn^Kv{j; z?!coNe(_1jvSN)b8q^lZ2r`c9+AQfhw625aNM&rQH?Snniwl4PK4A>*!qIymUYTfTq&vaHqtz+E}-0b2pD zl#JWHL55LmdjOA=@GC3$c7yeg0&XZ7w{qswS+JY~oL|D(ZL49y+vQaaGGcT(Nx@Ebjq(SMckTqaCQOo=@ACGLdCFh(fVX54ON6 zKWh8ePF3|7dN5P;sWAA?HIIS&fQ@TFB)J8^s+~%}+BO41)p5RFG3H8Omkhs(f1W4< zKm+i{kUm-_v26CuCM7Ne&PwJeyB4kxa0(J7OcXXPNMYtZli#JNeYcOALa8}a zLAEeNNgySIQnKyU6l%RA8L%I4ck;U&W3!dEF5qI|w1j6P;2pEB&8ggLV)Ty#yCpnj zxdJu!O2fiun?#SQyll@P*;qiC@w)|peRb?yKr!-yWDqrODoCHq)s1ozN@?YS)PhbR zC}>mQdBYb0&2b6UyNX;Q%H0#>e?J5tG0{j_ZPQp|6>ewBNZl8m30-eg?3X*QNQ<`ih v+s&km;}B>(Gi|>H0H)33M&AIylmp;j)FC}b`;oD?00000NkvXXu0mjflN&`# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..53b0a629d3bd5df3d95c8340e09e7afbbc26f115 GIT binary patch literal 951 zcmV;o14#UdP)98pq*`t3BLHgBPa41iKmce6 zs4}1)pkXx993VZRX?u)rJAT#>Fb!B-(|#pzMe&nrwXIJU0GYLv*|;j; z{S6$~y!5>@6%YWKLPa$b08uNOnU?^_6e_Bj0Ek-I%)A6ZrchDM1VGfvX67XTGKGq2 zCIF&VHZ!jj0P}#Iz*=C16YCrB5_k$c2EI1fuLm~tVl8*#*#jktHz?9v>-RT$3V?^e z)=Y@{X0L!#z_ps+X5cKaUGau0Fe?29zC;rMyMgPoELPwA9B=~oRq@{kTm}{v#8f<( zYyAYkgahgVU!8u*fEw|fcp zY`grM1sbJ(&r-B{01Vq1^d8s?+(`uYcDBfWbowRhEC410U;)t3OHEP_R1ClkU`wKl z{{S2Z?j_poL6Qsd?}UEqO6LF}X1Yout$;TNkaocp5CFjo?4>6FJcWX{3xMDS_Ri~A&-<};@Dto%9&kMkAeEoB!0N4mT z=snBdWMjL3&H$$YJ0f-$a2{BlIDJ(1|6dp?C7$AF)}Vc?>3 z?yu~JpS#?)J_muTxd51*?aHw|X-jHOwv73_7tGH3wQCb_60lp2vhSWgkAS1I83Mey zgW7{Apnm~D3R#+l04P;1h@SumQpnOY1VE{BLHqV2t55M`;nf#M z+N$3tm|5<$%Ys&4{HXTs>eILs+%IB~(M)NSDBukNB{AhQMhXakF)CIYUsV9Kq3RgL Z`~!8rZeNmGw$}gv002ovPDHLkV1gYKsRsZ6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7465f4abfab0178bdb8dfbeac9e21ae295d33a76 GIT binary patch literal 831 zcmV-F1Hk-=P)Ou;qTDKLeN%QYj5uluoJ{eRIn2)6twdXSlau6h##P$ zvUoa`JMnz)(~bc5E40!tD!y!V~1}o&fMxxN!Rdpzs8FyC(p=6)xPq04O{`-tJidTm+6APJ~Cm z_kkCV&W0bRoCA&-*Z3-51{K1F|tG7S0%oCh8_0+2cwkuSqZh~`-UtWRx?FbaV6 z^BmnF0D97>><+V1k}la5AT)t$y#7vbi4CdMs9(!%0%xq8GF74iRufpF&+=(T1q8s1 zV%BI%0MwYZ8GRN2Gm2TGDFIMp)@Jls0L&<6jixLBjsp84fqe&F0iQDi-vfl-?#uqL zfZqE2x~c`h*f8kbYC`uY69DUxs_c*dyLqZU0${@d761akz6IO`PDM)md*CwgH1ig4 z2)H5pQT4p_NvZ&uxz)B>6tJ4WTH=tOnF;N4SSk@qT{j7Uo50DQP+v+0-*5( zTj2=+OQFWs1wi8ow!#wtmO_oM3xLKGY=tKPEQK0h7XXbX*b2`Ap!>myx4;D;%m>2J z{~&|b%hhK)27(1ZcNp|~H3WLHGS+{Ft@YnSvJ&;lenm|8!;wFL@ZOs+;tWSH+Wj!2 z*2~o=%p?X6s{&ZJ5Up3PfB>jGfm7WV08%6ba4!+hWe!l%#^^oVMnXq{KkCUU(4TQ zZPcCqzpm4n!G`gh*_lbI|5yrkC;q>3_Zsu}r~4TM4#=^EFg`n?xv7bhf$_my{`39s4{$#a{lN2M*8#tLwlb#G4ZMuE?Vp@> zX}!mEo#FJ=J>dBbbn<3D%hv+6PZj=D8P|8bM;{4M%MoDIzf?oMm^ z|F(|XVrN44ADs(-(3?^20BCTh)cXZ8Om29x7i90q1g2UISK81T8>U%vQQa6@Zc zJWJL?Q+DTd+(FT6e$QIl8KuiKTiosIo2^W{_%FQvQd%CzP;!04buku)^_Q>Ttz^6q zdpIsyfMKs+eED-$hjQNPTt$|KyUY=koqCCvf*y_hWGDI|4U&Eer^1Fr6NO( zdb1rvdt>`Vh8DklC-(g>DGsk?ad^nU&A=!yk16MM!SuyP>RJ4l`<0hBY&{TqvwYR` zsQGT93P#(du8FfcbsyMwMA2SZRDfaPA>Py(3=&>y%8#*e_!+9bR=j1saoOzkS-$u^ z%=fInF}{)eHtW^uJ5sZkhyDNbk|}}xgGj`)8~Ph~7jPe_d~lsZfxSWdfLia|P&N0p zO$;(?=WTepy2QL8xFNKq$~x}u!{|qnYc|#&nK>=Ykzvi@)CB7dMJf}w@%|CoFxmXG z-^`G|+ZfXub)~%5MVT@sXuOs=`Y_PpKdVSzJA(j2t9C%ek5vpz3kq1d_~S(#7(~`N zJbbvCiGyK1(^qk~8*CR051iwQkgip1UiWg!P>x}5z~0+ a@PA(0RTl!Ra<75%A%mx@pUXO@geCxF!kHZa literal 0 HcmV?d00001 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 }, {