diff --git a/examples/website/highway/app.tsx b/examples/website/highway/app.tsx index 198866da7f0..466586c46ef 100644 --- a/examples/website/highway/app.tsx +++ b/examples/website/highway/app.tsx @@ -10,9 +10,10 @@ import {GeoJsonLayer} from '@deck.gl/layers'; import {scaleLinear, scaleThreshold} from 'd3-scale'; import {CSVLoader} from '@loaders.gl/csv'; import {load} from '@loaders.gl/core'; +import {Device} from '@luma.gl/core'; import {Feature, LineString, MultiLineString} from 'geojson'; -import type {Color, PickingInfo, MapViewState} from '@deck.gl/core'; +import type {Color, PickingInfo, MapViewState, Widget} from '@deck.gl/core'; // Source data GeoJSON const DATA_URL = { @@ -130,18 +131,23 @@ function renderTooltip({ } export default function App({ + device, roads = DATA_URL.ROADS, year, accidents, - mapStyle = MAP_STYLE + mapStyle = MAP_STYLE, + widgets }: { + device?: Device; roads?: string | Road[]; accidents?: Accident[]; year?: number; mapStyle?: string; + widgets?: Widget[]; }) { const [hoverInfo, setHoverInfo] = useState>(); const {incidents, fatalities} = useMemo(() => aggregateAccidents(accidents), [accidents]); + const isWebGPU = device?.type === 'webgpu'; const layers = [ new GeoJsonLayer({ @@ -183,10 +189,22 @@ export default function App({ return ( diff --git a/modules/core/src/shaderlib/project/project.wgsl.ts b/modules/core/src/shaderlib/project/project.wgsl.ts index 23053ce73c7..f7dc1da85f6 100644 --- a/modules/core/src/shaderlib/project/project.wgsl.ts +++ b/modules/core/src/shaderlib/project/project.wgsl.ts @@ -129,8 +129,8 @@ fn project_size_vec4(meters: vec4) -> vec4 { fn project_get_orientation_matrix(up: vec3) -> mat3x3 { let uz = normalize(up); let ux = select( - vec3(1.0, 0.0, 0.0), normalize(vec3(uz.y, -uz.x, 0.0)), + vec3(1.0, 0.0, 0.0), abs(uz.z) == 1.0 ); let uy = cross(uz, ux); diff --git a/modules/layers/src/path-layer/path-layer-uniforms.ts b/modules/layers/src/path-layer/path-layer-uniforms.ts index d04292ef991..6cf1a8a5153 100644 --- a/modules/layers/src/path-layer/path-layer-uniforms.ts +++ b/modules/layers/src/path-layer/path-layer-uniforms.ts @@ -4,7 +4,23 @@ import type {ShaderModule} from '@luma.gl/shadertools'; -const uniformBlock = `\ +const uniformBlockWGSL = /* wgsl */ `\ +struct PathUniforms { + widthScale: f32, + widthMinPixels: f32, + widthMaxPixels: f32, + jointType: f32, + capType: f32, + miterLimit: f32, + billboard: f32, + widthUnits: i32, +}; + +@group(0) @binding(auto) +var path: PathUniforms; +`; + +const uniformBlockGLSL = `\ layout(std140) uniform pathUniforms { float widthScale; float widthMinPixels; @@ -30,8 +46,9 @@ export type PathProps = { export const pathUniforms = { name: 'path', - vs: uniformBlock, - fs: uniformBlock, + source: uniformBlockWGSL, + vs: uniformBlockGLSL, + fs: uniformBlockGLSL, uniformTypes: { widthScale: 'f32', widthMinPixels: 'f32', diff --git a/modules/layers/src/path-layer/path-layer.ts b/modules/layers/src/path-layer/path-layer.ts index f68eeebc3d6..92e8e81623b 100644 --- a/modules/layers/src/path-layer/path-layer.ts +++ b/modules/layers/src/path-layer/path-layer.ts @@ -2,12 +2,14 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Layer, project32, picking, UNIT} from '@deck.gl/core'; +import {Layer, project32, color, picking, UNIT} from '@deck.gl/core'; +import {BufferLayout, Parameters} from '@luma.gl/core'; import {Geometry} from '@luma.gl/engine'; import {Model} from '@luma.gl/engine'; import PathTesselator from './path-tesselator'; import {pathUniforms, PathProps} from './path-layer-uniforms'; +import source from './path-layer.wgsl'; import vs from './path-layer-vertex.glsl'; import fs from './path-layer-fragment.glsl'; @@ -135,7 +137,7 @@ export default class PathLayer extends }; getShaders() { - return super.getShaders({vs, fs, modules: [project32, picking, pathUniforms]}); // 'project' module added by default. + return super.getShaders({vs, fs, source, modules: [project32, color, picking, pathUniforms]}); // 'project' module added by default. } get wrapLongitude(): boolean { @@ -143,67 +145,139 @@ export default class PathLayer extends } getBounds(): [number[], number[]] | null { + if (this.context.device.type === 'webgpu') { + return null; + } return this.getAttributeManager()?.getBounds(['vertexPositions']); } initializeState() { const noAlloc = true; const attributeManager = this.getAttributeManager(); + const enableTransitions = this.context.device.type !== 'webgpu'; /* eslint-disable max-len */ - attributeManager!.addInstanced({ - vertexPositions: { - size: 3, - // Start filling buffer from 1 vertex in - vertexOffset: 1, - type: 'float64', - fp64: this.use64bitPositions(), - transition: ATTRIBUTE_TRANSITION, - accessor: 'getPath', - // eslint-disable-next-line @typescript-eslint/unbound-method - update: this.calculatePositions, - noAlloc, - shaderAttributes: { - instanceLeftPositions: { - vertexOffset: 0 - }, - instanceStartPositions: { - vertexOffset: 1 + if (this.context.device.type === 'webgpu') { + attributeManager!.addInstanced({ + instancePositions: { + size: 12, + type: 'float32', + // WebGPU keeps the fp64 path by uploading float32 high parts and reconstructing + // with the matching `instancePositions64Low` residuals in WGSL. + transition: false, + accessor: 'getPath', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateInstancePositions, + shaderAttributes: { + instanceLeftPositions: {size: 3, elementOffset: 0}, + instanceStartPositions: {size: 3, elementOffset: 3}, + instanceEndPositions: {size: 3, elementOffset: 6}, + instanceRightPositions: {size: 3, elementOffset: 9} }, - instanceEndPositions: { - vertexOffset: 2 + noAlloc + }, + instancePositions64Low: { + size: 12, + type: 'float32', + // This is the low-part companion to `instancePositions`, not a plain-fp32 downgrade. + transition: false, + accessor: 'getPath', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateInstancePositions64Low, + shaderAttributes: { + instanceLeftPositions64Low: {size: 3, elementOffset: 0}, + instanceStartPositions64Low: {size: 3, elementOffset: 3}, + instanceEndPositions64Low: {size: 3, elementOffset: 6}, + instanceRightPositions64Low: {size: 3, elementOffset: 9} }, - instanceRightPositions: { - vertexOffset: 3 + noAlloc + }, + instanceTypes: { + size: 1, + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateSegmentTypes, + noAlloc + }, + instanceStrokeWidths: { + size: 1, + accessor: 'getWidth', + transition: false, + defaultValue: 1 + }, + instanceColors: { + size: this.props.colorFormat.length, + type: 'unorm8', + accessor: 'getColor', + transition: false, + defaultValue: DEFAULT_COLOR + }, + instancePickingColors: { + size: 4, + type: 'uint8', + accessor: (object, {index, target: value}) => + this.encodePickingColor( + object && object.__source ? object.__source.index : index, + value + ) + } + }); + } else { + attributeManager!.addInstanced({ + vertexPositions: { + size: 3, + // Start filling buffer from 1 vertex in + vertexOffset: 1, + type: 'float64', + fp64: this.use64bitPositions(), + transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, + accessor: 'getPath', + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculatePositions, + noAlloc, + shaderAttributes: { + instanceLeftPositions: { + vertexOffset: 0 + }, + instanceStartPositions: { + vertexOffset: 1 + }, + instanceEndPositions: { + vertexOffset: 2 + }, + instanceRightPositions: { + vertexOffset: 3 + } } + }, + instanceTypes: { + size: 1, + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateSegmentTypes, + noAlloc + }, + instanceStrokeWidths: { + size: 1, + accessor: 'getWidth', + transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, + defaultValue: 1 + }, + instanceColors: { + size: this.props.colorFormat.length, + type: 'unorm8', + accessor: 'getColor', + transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, + defaultValue: DEFAULT_COLOR + }, + instancePickingColors: { + size: 4, + type: 'uint8', + accessor: (object, {index, target: value}) => + this.encodePickingColor( + object && object.__source ? object.__source.index : index, + value + ) } - }, - instanceTypes: { - size: 1, - type: 'uint8', - // eslint-disable-next-line @typescript-eslint/unbound-method - update: this.calculateSegmentTypes, - noAlloc - }, - instanceStrokeWidths: { - size: 1, - accessor: 'getWidth', - transition: ATTRIBUTE_TRANSITION, - defaultValue: 1 - }, - instanceColors: { - size: this.props.colorFormat.length, - type: 'unorm8', - accessor: 'getColor', - transition: ATTRIBUTE_TRANSITION, - defaultValue: DEFAULT_COLOR - }, - instancePickingColors: { - size: 4, - type: 'uint8', - accessor: (object, {index, target: value}) => - this.encodePickingColor(object && object.__source ? object.__source.index : index, value) - } - }); + }); + } /* eslint-enable max-len */ this.setState({ @@ -317,6 +391,14 @@ export default class PathLayer extends } protected _getModel(): Model { + const parameters = + this.context.device.type === 'webgpu' + ? ({ + depthWriteEnabled: true, + depthCompare: 'less-equal' + } satisfies Parameters) + : undefined; + /* * _ * "-_ 1 3 5 @@ -364,7 +446,7 @@ export default class PathLayer extends return new Model(this.context.device, { ...this.getShaders(), id: this.props.id, - bufferLayout: this.getAttributeManager()!.getBufferLayouts(), + bufferLayout: this._getModelBufferLayouts(), geometry: new Geometry({ topology: 'triangle-list', attributes: { @@ -372,6 +454,7 @@ export default class PathLayer extends positions: {value: new Float32Array(SEGMENT_POSITIONS), size: 2} } }), + parameters, isInstanced: true }); } @@ -383,10 +466,71 @@ export default class PathLayer extends attribute.value = pathTesselator.get('positions'); } + protected calculateInstancePositions(attribute) { + this._calculateInterleavedInstancePositions(attribute, false); + } + + protected calculateInstancePositions64Low(attribute) { + this._calculateInterleavedInstancePositions(attribute, true); + } + protected calculateSegmentTypes(attribute) { const {pathTesselator} = this.state; attribute.startIndices = pathTesselator.vertexStarts; attribute.value = pathTesselator.get('segmentTypes'); } + + protected _calculateInterleavedInstancePositions(attribute, lowPart: boolean) { + const {pathTesselator} = this.state; + const value = pathTesselator.get('positions'); + + if (!value) { + attribute.value = null; + return; + } + + const numInstances = pathTesselator.instanceCount; + const result = new Float32Array(numInstances * 12); + // WebGL reads a padded neighbor window using `vertexOffset: 1`; this materializes + // the same [-1, 0, 1, 2] access pattern explicitly for the WebGPU layout. + const neighborOffsets = [-1, 0, 1, 2]; + + for (let i = 0; i < numInstances; i++) { + const targetIndex = i * 12; + for (let vertexOffset = 0; vertexOffset < 4; vertexOffset++) { + const sourceVertex = i + neighborOffsets[vertexOffset]; + const targetOffset = targetIndex + vertexOffset * 3; + for (let j = 0; j < 3; j++) { + const position = + sourceVertex >= 0 && sourceVertex < numInstances ? value[sourceVertex * 3 + j] : 0; + result[targetOffset + j] = lowPart ? position - Math.fround(position) : position; + } + } + } + + attribute.startIndices = pathTesselator.vertexStarts; + attribute.value = result; + } + + protected _getModelBufferLayouts(): BufferLayout[] { + const bufferLayouts = this.getAttributeManager()!.getBufferLayouts(); + + if (this.context.device.type === 'webgpu') { + return bufferLayouts; + } + + return bufferLayouts.map(layout => + layout.name === 'vertexPositions' + ? { + ...layout, + attributes: (layout.attributes || []).filter( + attribute => + attribute.attribute !== 'vertexPositions' && + attribute.attribute !== 'vertexPositions64Low' + ) + } + : layout + ); + } } diff --git a/modules/layers/src/path-layer/path-layer.wgsl.ts b/modules/layers/src/path-layer/path-layer.wgsl.ts new file mode 100644 index 00000000000..825aa3a63b7 --- /dev/null +++ b/modules/layers/src/path-layer/path-layer.wgsl.ts @@ -0,0 +1,287 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export default /* wgsl */ `\ +const EPSILON: f32 = 0.001; +const ZERO_OFFSET: vec3 = vec3(0.0, 0.0, 0.0); + +struct JoinResult { + offset: vec3, + cornerOffset: vec2, + miterLength: f32, + pathPosition: vec2, + pathLength: f32, + jointType: f32, +}; + +struct Attributes { + @location(0) positions: vec2, + @location(1) instanceTypes: f32, + @location(2) instanceLeftPositions: vec3, + @location(3) instanceStartPositions: vec3, + @location(4) instanceEndPositions: vec3, + @location(5) instanceRightPositions: vec3, + @location(6) instanceLeftPositions64Low: vec3, + @location(7) instanceStartPositions64Low: vec3, + @location(8) instanceEndPositions64Low: vec3, + @location(9) instanceRightPositions64Low: vec3, + @location(10) instanceStrokeWidths: f32, + @location(11) instanceColors: vec4, + @location(12) instancePickingColors: vec3, +}; + +struct Varyings { + @builtin(position) position: vec4, + @location(0) vColor: vec4, + @location(1) vCornerOffset: vec2, + @location(2) vMiterLength: f32, + @location(3) vPathPosition: vec2, + @location(4) vPathLength: f32, + @location(5) vJointType: f32, + @location(6) pickingColor: vec3, +}; + +fn flipIfTrue(flag: bool) -> f32 { + return select(1.0, -1.0, flag); +} + +fn clipLine(position: vec4, refPosition: vec4) -> vec4 { + if (position.w < EPSILON) { + let r = (EPSILON - refPosition.w) / (position.w - refPosition.w); + return refPosition + (position - refPosition) * r; + } + return position; +} + +fn getLineJoinOffset( + prevPoint: vec3, + currPoint: vec3, + nextPoint: vec3, + width: vec2, + positions: vec2, + instanceTypes: f32 +) -> JoinResult { + let isEnd = positions.x > 0.0; + let sideOfPath = positions.y; + let isJoint = select(0.0, 1.0, sideOfPath == 0.0); + + var deltaA3 = currPoint - prevPoint; + var deltaB3 = nextPoint - currPoint; + + let rotationResult = project_needs_rotation(currPoint); + if (path.billboard == 0.0 && rotationResult.needsRotation) { + deltaA3 = rotationResult.transform * deltaA3; + deltaB3 = rotationResult.transform * deltaB3; + } + + let deltaA = deltaA3.xy / width; + let deltaB = deltaB3.xy / width; + + let lenA = length(deltaA); + let lenB = length(deltaB); + + let dirA = select(vec2(0.0, 0.0), normalize(deltaA), lenA > 0.0); + let dirB = select(vec2(0.0, 0.0), normalize(deltaB), lenB > 0.0); + + let perpA = vec2(-dirA.y, dirA.x); + let perpB = vec2(-dirB.y, dirB.x); + + var tangent = dirA + dirB; + tangent = select(perpA, normalize(tangent), length(tangent) > 0.0); + let miterVec = vec2(-tangent.y, tangent.x); + let dir = select(dirB, dirA, isEnd); + let perp = select(perpB, perpA, isEnd); + let pathLength = select(lenB, lenA, isEnd); + + let sinHalfA = abs(dot(miterVec, perp)); + let cosHalfA = abs(dot(dirA, miterVec)); + let turnDirection = flipIfTrue(dirA.x * dirB.y >= dirA.y * dirB.x); + let cornerPosition = sideOfPath * turnDirection; + + var miterSize = 1.0 / max(sinHalfA, EPSILON); + miterSize = mix( + min(miterSize, max(lenA, lenB) / max(cosHalfA, EPSILON)), + miterSize, + step(0.0, cornerPosition) + ); + + var offsetVec = + mix(miterVec * miterSize, perp, step(0.5, cornerPosition)) * + (sideOfPath + isJoint * turnDirection); + + let isStartCap = lenA == 0.0 || (!isEnd && (instanceTypes == 1.0 || instanceTypes == 3.0)); + let isEndCap = lenB == 0.0 || (isEnd && (instanceTypes == 2.0 || instanceTypes == 3.0)); + let isCap = isStartCap || isEndCap; + + var jointType = path.jointType; + if (isCap) { + offsetVec = mix( + perp * sideOfPath, + dir * path.capType * 4.0 * flipIfTrue(isStartCap), + isJoint + ); + jointType = path.capType; + } + + var miterLength = dot(offsetVec, miterVec * turnDirection); + miterLength = select(miterLength, isJoint, isCap); + + let offsetFromStartOfPath = offsetVec + deltaA * select(0.0, 1.0, isEnd); + let pathPosition = vec2( + dot(offsetFromStartOfPath, perp), + dot(offsetFromStartOfPath, dir) + ); + let isValid = step(f32(instanceTypes), 3.5); + var offset = vec3(offsetVec * width * isValid, 0.0); + + if (path.billboard == 0.0 && rotationResult.needsRotation) { + offset = rotationResult.transform * offset; + } + + return JoinResult(offset, offsetVec, miterLength, pathPosition, pathLength, jointType); +} + +@vertex +fn vertexMain(attributes: Attributes) -> Varyings { + var varyings: Varyings; + + geometry.pickingColor = attributes.instancePickingColors; + + let isEnd = attributes.positions.x; + + let prevPosition = mix(attributes.instanceLeftPositions, attributes.instanceStartPositions, isEnd); + let prevPosition64Low = mix( + attributes.instanceLeftPositions64Low, + attributes.instanceStartPositions64Low, + isEnd + ); + let currPosition = mix(attributes.instanceStartPositions, attributes.instanceEndPositions, isEnd); + let currPosition64Low = mix( + attributes.instanceStartPositions64Low, + attributes.instanceEndPositions64Low, + isEnd + ); + let nextPosition = mix(attributes.instanceEndPositions, attributes.instanceRightPositions, isEnd); + let nextPosition64Low = mix( + attributes.instanceEndPositions64Low, + attributes.instanceRightPositions64Low, + isEnd + ); + + geometry.worldPosition = currPosition; + let currPositionCommon = project_position_vec3_f64(currPosition, currPosition64Low); + geometry.position = vec4(currPositionCommon, 1.0); + + let widthPixels = + clamp( + project_unit_size_to_pixel(attributes.instanceStrokeWidths * path.widthScale, path.widthUnits), + path.widthMinPixels, + path.widthMaxPixels + ) / 2.0; + + if (path.billboard != 0.0) { + var prevPositionScreen = project_position_to_clipspace(prevPosition, prevPosition64Low, ZERO_OFFSET); + var currPositionScreen = project_position_to_clipspace(currPosition, currPosition64Low, ZERO_OFFSET); + var nextPositionScreen = project_position_to_clipspace(nextPosition, nextPosition64Low, ZERO_OFFSET); + + prevPositionScreen = clipLine(prevPositionScreen, currPositionScreen); + nextPositionScreen = clipLine(nextPositionScreen, currPositionScreen); + currPositionScreen = clipLine(currPositionScreen, mix(nextPositionScreen, prevPositionScreen, isEnd)); + + let join = getLineJoinOffset( + prevPositionScreen.xyz / prevPositionScreen.w, + currPositionScreen.xyz / currPositionScreen.w, + nextPositionScreen.xyz / nextPositionScreen.w, + project_pixel_size_to_clipspace(vec2(widthPixels, widthPixels)), + attributes.positions, + attributes.instanceTypes + ); + + geometry.uv = join.pathPosition; + varyings.position = vec4( + currPositionScreen.xyz + join.offset * currPositionScreen.w, + currPositionScreen.w + ); + varyings.vCornerOffset = join.cornerOffset; + varyings.vMiterLength = join.miterLength; + varyings.vPathPosition = join.pathPosition; + varyings.vPathLength = join.pathLength; + varyings.vJointType = join.jointType; + } else { + let prevPositionCommon = project_position_vec3_f64(prevPosition, prevPosition64Low); + let nextPositionCommon = project_position_vec3_f64(nextPosition, nextPosition64Low); + + let width = vec2( + project_pixel_size_float(widthPixels), + project_pixel_size_float(widthPixels) + ); + let join = getLineJoinOffset( + prevPositionCommon, + currPositionCommon, + nextPositionCommon, + width, + attributes.positions, + attributes.instanceTypes + ); + + geometry.position = vec4(currPositionCommon + join.offset, 1.0); + geometry.uv = join.pathPosition; + varyings.position = project_common_position_to_clipspace(geometry.position); + varyings.vCornerOffset = join.cornerOffset; + varyings.vMiterLength = join.miterLength; + varyings.vPathPosition = join.pathPosition; + varyings.vPathLength = join.pathLength; + varyings.vJointType = join.jointType; + } + + varyings.vColor = vec4( + attributes.instanceColors.rgb, + attributes.instanceColors.a * layer.opacity + ); + varyings.pickingColor = attributes.instancePickingColors; + return varyings; +} + +@fragment +fn fragmentMain(varyings: Varyings) -> @location(0) vec4 { + geometry.uv = varyings.vPathPosition; + + if (varyings.vPathPosition.y < 0.0 || varyings.vPathPosition.y > varyings.vPathLength) { + if (varyings.vJointType > 0.5 && length(varyings.vCornerOffset) > 1.0) { + discard; + } + if (varyings.vJointType < 0.5 && varyings.vMiterLength > path.miterLimit + 1.0) { + discard; + } + } + + var fragColor = varyings.vColor; + + if (picking.isActive > 0.5) { + if (!picking_isColorValid(varyings.pickingColor)) { + discard; + } + return vec4(varyings.pickingColor, 1.0); + } + + if (picking.isHighlightActive > 0.5) { + let highlightedObjectColor = picking_normalizeColor(picking.highlightedObjectColor); + if (picking_isColorZero(abs(varyings.pickingColor - highlightedObjectColor))) { + let highLightAlpha = picking.highlightColor.a; + let blendedAlpha = highLightAlpha + fragColor.a * (1.0 - highLightAlpha); + if (blendedAlpha > 0.0) { + let highLightRatio = highLightAlpha / blendedAlpha; + fragColor = vec4( + mix(fragColor.rgb, picking.highlightColor.rgb, highLightRatio), + blendedAlpha + ); + } else { + fragColor = vec4(fragColor.rgb, 0.0); + } + } + } + + return deckgl_premultiplied_alpha(fragColor); +} +`; diff --git a/modules/layers/src/path-layer/path-tesselator.ts b/modules/layers/src/path-layer/path-tesselator.ts index f1ad0fb5818..3f46247b0cb 100644 --- a/modules/layers/src/path-layer/path-tesselator.ts +++ b/modules/layers/src/path-layer/path-tesselator.ts @@ -36,7 +36,9 @@ export default class PathTesselator extends Tesselator< initialize: true, type: opts.fp64 ? Float64Array : Float32Array }, - segmentTypes: {size: 1, type: Uint8ClampedArray} + // WebGPU currently consumes `instanceTypes` as f32; using Float32Array keeps the + // upload and vertex layout 4-byte aligned without introducing a padded uint8 path. + segmentTypes: {size: 1, type: Float32Array} } }); } diff --git a/website/src/examples/geojson-layer-paths.js b/website/src/examples/geojson-layer-paths.js index 4cf3b71b160..4b069264065 100644 --- a/website/src/examples/geojson-layer-paths.js +++ b/website/src/examples/geojson-layer-paths.js @@ -3,6 +3,7 @@ // Copyright (c) vis.gl contributors import React, {Component} from 'react'; +import {_StatsWidget as StatsWidget} from '@deck.gl/widgets'; import {readableInteger} from '../utils/format-utils'; import {MAPBOX_STYLES, DATA_URI, GITHUB_TREE} from '../constants/defaults'; import App, {COLOR_SCALE} from 'website-examples/highway/app'; @@ -25,6 +26,8 @@ class HighwayDemo extends Component { static code = `${GITHUB_TREE}/examples/website/highway`; + static hasDeviceTabs = true; + static parameters = { year: {displayName: 'Year', type: 'range', value: 1990, step: 5, min: 1990, max: 2015} }; @@ -80,10 +83,14 @@ class HighwayDemo extends Component { render() { const {data, params, ...otherProps} = this.props; + const widgets = [new StatsWidget({type: 'device'})]; return (