From 31dae929f5e7e3f4d88cea70c2d5b1b8668935ee Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 24 Oct 2019 13:46:03 +0200 Subject: [PATCH] ShaderBuilder / adapt logic & tests to new expressions module --- src/ol/webgl/ShaderBuilder.js | 371 +++-------------------- test/spec/ol/webgl/shaderbuilder.test.js | 297 ++---------------- 2 files changed, 61 insertions(+), 607 deletions(-) diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js index 6a90a01c4d..de3ccf8ce2 100644 --- a/src/ol/webgl/ShaderBuilder.js +++ b/src/ol/webgl/ShaderBuilder.js @@ -3,318 +3,7 @@ * @module ol/webgl/ShaderBuilder */ -import {asArray, isStringColor} from '../color.js'; - -/** - * Will return the number as a float with a dot separator, which is required by GLSL. - * @param {number} v Numerical value. - * @returns {string} The value as string. - */ -export function formatNumber(v) { - const s = v.toString(); - return s.indexOf('.') === -1 ? s + '.0' : s; -} - -/** - * Will return the number array as a float with a dot separator, concatenated with ', '. - * @param {Array} array Numerical values array. - * @returns {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`. - */ -export function formatArray(array) { - if (array.length < 2 || array.length > 4) { - throw new Error('`formatArray` can only output `vec2`, `vec3` or `vec4` arrays.'); - } - return `vec${array.length}(${array.map(formatNumber).join(', ')})`; -} - -/** - * Will normalize and converts to string a `vec4` color array compatible with GLSL. - * @param {string|import("../color.js").Color} color Color either in string format or [r, g, b, a] array format, - * with RGB components in the 0..255 range and the alpha component in the 0..1 range. - * Note that the final array will always have 4 components. - * @returns {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form. - */ -export function formatColor(color) { - const array = asArray(color).slice(); - if (array.length < 4) { - array.push(1); - } - return formatArray( - array.map(function(c, i) { - return i < 3 ? c / 255 : c; - }) - ); -} - -/** - * Possible inferred types from a given value or expression - * @enum {number} - */ -export const ValueTypes = { - UNKNOWN: -1, - NUMBER: 0, - STRING: 1, - COLOR: 2, - COLOR_OR_STRING: 3 -}; - -function getValueType(value) { - if (typeof value === 'number') { - return ValueTypes.NUMBER; - } - if (typeof value === 'string') { - if (isStringColor(value)) { - return ValueTypes.COLOR_OR_STRING; - } - return ValueTypes.STRING; - } - if (!Array.isArray(value)) { - throw new Error(`Unrecognized value type: ${JSON.stringify(value)}`); - } - if (value.length === 3 || value.length === 4) { - const onlyNumbers = value.every(function(v) { - return typeof v === 'number'; - }); - if (onlyNumbers) { - return ValueTypes.COLOR; - } - } - if (typeof value[0] !== 'string') { - return ValueTypes.UNKNOWN; - } - switch (value[0]) { - case 'get': - case 'var': - case 'time': - case '*': - case '/': - case '+': - case '-': - case 'clamp': - case 'stretch': - case 'mod': - case 'pow': - case '>': - case '>=': - case '<': - case '<=': - case '==': - case '!': - case 'between': - return ValueTypes.NUMBER; - case 'interpolate': - return ValueTypes.COLOR; - default: - return ValueTypes.UNKNOWN; - } -} - -/** - * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. - * @returns {boolean} True if a numeric value, false otherwise - */ -export function isValueTypeNumber(value) { - return getValueType(value) === ValueTypes.NUMBER; -} - -/** - * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. - * @returns {boolean} True if a string value, false otherwise - */ -export function isValueTypeString(value) { - return getValueType(value) === ValueTypes.STRING || getValueType(value) === ValueTypes.COLOR_OR_STRING; -} - -/** - * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. - * @returns {boolean} True if a color value, false otherwise - */ -export function isValueTypeColor(value) { - return getValueType(value) === ValueTypes.COLOR || getValueType(value) === ValueTypes.COLOR_OR_STRING; -} - - -/** - * Check that the provided value or expression is valid, and that the types used are compatible. - * - * Will throw an exception if found to be invalid. - * - * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. - */ -export function check(value) { - // these will be used to validate types in the expressions - function checkNumber(value) { - if (!isValueTypeNumber(value)) { - throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`); - } - } - function checkColor(value) { - if (!isValueTypeColor(value)) { - throw new Error(`A color value was expected, got ${JSON.stringify(value)} instead`); - } - } - function checkString(value) { - if (!isValueTypeString(value)) { - throw new Error(`A string value was expected, got ${JSON.stringify(value)} instead`); - } - } - - // first check that the value is of a recognized kind - if (!isValueTypeColor(value) && !isValueTypeNumber(value) && !isValueTypeString(value)) { - throw new Error(`No type could be inferred from the following expression: ${JSON.stringify(value)}`); - } - - // check operator arguments - if (Array.isArray(value) && typeof value[0] === 'string') { - switch (value[0]) { - case 'get': - case 'var': - checkString(value[1]); - break; - case 'time': - break; - case '*': - case '/': - case '+': - case '-': - case 'mod': - case 'pow': - checkNumber(value[1]); - checkNumber(value[2]); - break; - case 'clamp': - checkNumber(value[1]); - checkNumber(value[2]); - checkNumber(value[3]); - break; - case 'stretch': - checkNumber(value[1]); - checkNumber(value[2]); - checkNumber(value[3]); - checkNumber(value[4]); - checkNumber(value[5]); - break; - case '>': - case '>=': - case '<': - case '<=': - case '==': - checkNumber(value[1]); - checkNumber(value[2]); - break; - case '!': - checkNumber(value[1]); - break; - case 'between': - checkNumber(value[1]); - checkNumber(value[2]); - checkNumber(value[3]); - break; - case 'interpolate': - checkNumber(value[1]); - checkColor(value[2]); - checkColor(value[3]); - break; - default: throw new Error(`Unrecognized operator in style expression: ${JSON.stringify(value)}`); - } - } -} - -/** - * Parses the provided expressions and produces a GLSL-compatible assignment string, such as: - * `['add', ['*', ['get', 'size'], 0.001], 12] => '(a_size * (0.001)) + (12.0)' - * - * Also takes in two arrays where new attributes and variables will be pushed, so that the user of the `parse` function - * knows which attributes/variables are expected to be available at evaluation time. - * - * For attributes, a prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in - * the final assignment string (depending on whether we're outputting a vertex or fragment shader). - * - * If a wrong value type is supplied to an operator (i. e. using colors with the `clamp` operator), an exception - * will be thrown. - * - * Note that by default, the `string` value type will be given precedence over `color`, so for example the - * `'yellow'` literal value will be parsed as a `string` while being a valid CSS color. This can be changed with - * the `typeHint` optional parameter which disambiguates what kind of value is expected. - * - * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. - * @param {Array} attributes Array containing the attribute names **without a prefix**; - * it is passed along recursively - * @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`). - * @param {Array} variables Array containing the variable names **without a prefix**; - * it is passed along recursively - * @param {ValueTypes} [typeHint] Hint for inferred type - * @returns {string} Assignment string. - */ -export function parse(value, attributes, attributePrefix, variables, typeHint) { - check(value); - - function p(value) { - return parse(value, attributes, attributePrefix, variables); - } - function pC(value) { - return parse(value, attributes, attributePrefix, variables, ValueTypes.COLOR); - } - - // operator - if (Array.isArray(value) && typeof value[0] === 'string') { - switch (value[0]) { - // reading operators - case 'get': - if (attributes.indexOf(value[1]) === -1) { - attributes.push(value[1]); - } - return attributePrefix + value[1]; - case 'var': - if (variables.indexOf(value[1]) === -1) { - variables.push(value[1]); - } - return `u_${value[1]}`; - case 'time': - return 'u_time'; - - // math operators - case '*': - case '/': - case '+': - case '-': - return `(${p(value[1])} ${value[0]} ${p(value[2])})`; - case 'clamp': return `clamp(${p(value[1])}, ${p(value[2])}, ${p(value[3])})`; - case 'stretch': - const low1 = p(value[2]); - const high1 = p(value[3]); - const low2 = p(value[4]); - const high2 = p(value[5]); - return `((clamp(${p(value[1])}, ${low1}, ${high1}) - ${low1}) * ((${high2} - ${low2}) / (${high1} - ${low1})) + ${low2})`; - case 'mod': return `mod(${p(value[1])}, ${p(value[2])})`; - case 'pow': return `pow(${p(value[1])}, ${p(value[2])})`; - - // color operators - case 'interpolate': - return `mix(${pC(value[2])}, ${pC(value[3])}, ${p(value[1])})`; - - // logical operators - case '>': - case '>=': - case '<': - case '<=': - case '==': - return `(${p(value[1])} ${value[0]} ${p(value[2])} ? 1.0 : 0.0)`; - case '!': - return `(${p(value[1])} > 0.0 ? 0.0 : 1.0)`; - case 'between': - return `(${p(value[1])} >= ${p(value[2])} && ${p(value[1])} <= ${p(value[3])} ? 1.0 : 0.0)`; - - default: throw new Error('Invalid style expression: ' + JSON.stringify(value)); - } - } else if (isValueTypeNumber(value)) { - return formatNumber(/** @type {number} */(value)); - } else if (isValueTypeString(value) && (typeHint === undefined || typeHint == ValueTypes.STRING)) { - return `"${value}"`; - } else { - return formatColor(/** @type {number[]|string} */(value)); - } -} +import {expressionToGlsl, ValueTypes} from '../style/expressions.js'; /** * @typedef {Object} VaryingDescription @@ -660,21 +349,30 @@ export function parseLiteralStyle(style) { const offset = symbStyle.offset || [0, 0]; const opacity = symbStyle.opacity !== undefined ? symbStyle.opacity : 1; - const variables = []; - const vertAttributes = []; - // parse function for vertex shader - function pVert(value) { - return parse(value, vertAttributes, 'a_', variables); - } + const vertContext = { + inFragmentShader: false, + variables: [], + attributes: [] + }; + const parseSizeX = expressionToGlsl(vertContext, size[0], ValueTypes.NUMBER); + const parseSizeY = expressionToGlsl(vertContext, size[1], ValueTypes.NUMBER); + const parsedOffsetX = expressionToGlsl(vertContext, offset[0], ValueTypes.NUMBER); + const parsedOffsetY = expressionToGlsl(vertContext, offset[1], ValueTypes.NUMBER); + const parsedTexCoordU1 = expressionToGlsl(vertContext, texCoord[0], ValueTypes.NUMBER); + const parsedTexCoordV1 = expressionToGlsl(vertContext, texCoord[1], ValueTypes.NUMBER); + const parsedTexCoordU2 = expressionToGlsl(vertContext, texCoord[2], ValueTypes.NUMBER); + const parsedTexCoordV2 = expressionToGlsl(vertContext, texCoord[3], ValueTypes.NUMBER); - const fragAttributes = []; - // parse function for fragment shader - function pFrag(value, type) { - return parse(value, fragAttributes, 'v_', variables, type); - } + const fragContext = { + inFragmentShader: true, + variables: vertContext.variables, + attributes: [] + }; + const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR); + const parsedOpacity = expressionToGlsl(fragContext, opacity, ValueTypes.NUMBER); let opacityFilter = '1.0'; - const visibleSize = pFrag(size[0]); + const visibleSize = expressionToGlsl(fragContext, size[0], ValueTypes.NUMBER); switch (symbStyle.symbolType) { case 'square': break; case 'image': break; @@ -691,26 +389,25 @@ export function parseLiteralStyle(style) { default: throw new Error('Unexpected symbol type: ' + symbStyle.symbolType); } - const parsedColor = pFrag(color, ValueTypes.COLOR); - const builder = new ShaderBuilder() - .setSizeExpression(`vec2(${pVert(size[0])}, ${pVert(size[1])})`) - .setSymbolOffsetExpression(`vec2(${pVert(offset[0])}, ${pVert(offset[1])})`) + .setSizeExpression(`vec2(${parseSizeX}, ${parseSizeY})`) + .setSymbolOffsetExpression(`vec2(${parsedOffsetX}, ${parsedOffsetY})`) .setTextureCoordinateExpression( - `vec4(${pVert(texCoord[0])}, ${pVert(texCoord[1])}, ${pVert(texCoord[2])}, ${pVert(texCoord[3])})`) + `vec4(${parsedTexCoordU1}, ${parsedTexCoordV1}, ${parsedTexCoordU2}, ${parsedTexCoordV2})`) .setSymbolRotateWithView(!!symbStyle.rotateWithView) .setColorExpression( - `vec4(${parsedColor}.rgb, ${parsedColor}.a * ${pFrag(opacity)} * ${opacityFilter})`); + `vec4(${parsedColor}.rgb, ${parsedColor}.a * ${parsedOpacity} * ${opacityFilter})`); if (style.filter) { - builder.setFragmentDiscardExpression(`${pFrag(style.filter)} <= 0.0`); + const parsedFilter = expressionToGlsl(fragContext, style.filter, ValueTypes.BOOLEAN); + builder.setFragmentDiscardExpression(`!${parsedFilter}`); } /** @type {Object.} */ const uniforms = {}; // define one uniform per variable - variables.forEach(function(varName) { + fragContext.variables.forEach(function(varName) { builder.addUniform(`float u_${varName}`); uniforms[`u_${varName}`] = function() { return style.variables && style.variables[varName] !== undefined ? @@ -729,21 +426,21 @@ export function parseLiteralStyle(style) { // for each feature attribute used in the fragment shader, define a varying that will be used to pass data // from the vertex to the fragment shader, as well as an attribute in the vertex shader (if not already present) - fragAttributes.forEach(function(attrName) { - if (vertAttributes.indexOf(attrName) === -1) { - vertAttributes.push(attrName); + fragContext.attributes.forEach(function(attrName) { + if (vertContext.attributes.indexOf(attrName) === -1) { + vertContext.attributes.push(attrName); } builder.addVarying(`v_${attrName}`, 'float', `a_${attrName}`); }); // for each feature attribute used in the vertex shader, define an attribute in the vertex shader. - vertAttributes.forEach(function(attrName) { + vertContext.attributes.forEach(function(attrName) { builder.addAttribute(`float a_${attrName}`); }); return { builder: builder, - attributes: vertAttributes.map(function(attributeName) { + attributes: vertContext.attributes.map(function(attributeName) { return { name: attributeName, callback: function(feature) { diff --git a/test/spec/ol/webgl/shaderbuilder.test.js b/test/spec/ol/webgl/shaderbuilder.test.js index bc94e59bdc..0e136b251a 100644 --- a/test/spec/ol/webgl/shaderbuilder.test.js +++ b/test/spec/ol/webgl/shaderbuilder.test.js @@ -1,112 +1,17 @@ -import { - check, - formatArray, - formatColor, - formatNumber, - isValueTypeColor, - isValueTypeNumber, - isValueTypeString, - parse, - parseLiteralStyle, - ShaderBuilder, - ValueTypes -} from '../../../../src/ol/webgl/ShaderBuilder.js'; +import {parseLiteralStyle, ShaderBuilder} from '../../../../src/ol/webgl/ShaderBuilder.js'; +import {arrayToGlsl, colorToGlsl, numberToGlsl} from '../../../../src/ol/style/expressions.js'; describe('ol.webgl.ShaderBuilder', function() { - describe('formatNumber', function() { - it('does a simple transform when a fraction is present', function() { - expect(formatNumber(1.3456)).to.eql('1.3456'); - }); - it('adds a fraction separator when missing', function() { - expect(formatNumber(1)).to.eql('1.0'); - expect(formatNumber(2.0)).to.eql('2.0'); - }); - }); - - describe('formatArray', function() { - it('outputs numbers with dot separators', function() { - expect(formatArray([1, 0, 3.45, 0.8888])).to.eql('vec4(1.0, 0.0, 3.45, 0.8888)'); - expect(formatArray([3, 4])).to.eql('vec2(3.0, 4.0)'); - }); - it('throws on invalid lengths', function() { - let thrown = false; - try { - formatArray([3]); - } catch (e) { - thrown = true; - } - try { - formatArray([3, 2, 1, 0, -1]); - } catch (e) { - thrown = true; - } - expect(thrown).to.be(true); - }); - }); - - describe('formatColor', function() { - it('normalizes color and outputs numbers with dot separators', function() { - expect(formatColor([100, 0, 255])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)'); - expect(formatColor([100, 0, 255, 1])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)'); - }); - it('handles colors in string format', function() { - expect(formatColor('red')).to.eql('vec4(1.0, 0.0, 0.0, 1.0)'); - expect(formatColor('#00ff99')).to.eql('vec4(0.0, 1.0, 0.6, 1.0)'); - expect(formatColor('rgb(100, 0, 255)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)'); - expect(formatColor('rgba(100, 0, 255, 0.3)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 0.3)'); - }); - }); - - describe('value type checking', function() { - it('correctly recognizes a number value', function() { - expect(isValueTypeNumber(1234)).to.eql(true); - expect(isValueTypeNumber(['time'])).to.eql(true); - expect(isValueTypeNumber(['clamp', ['get', 'attr'], -1, 1])).to.eql(true); - expect(isValueTypeNumber(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(false); - expect(isValueTypeNumber('yellow')).to.eql(false); - expect(isValueTypeNumber('#113366')).to.eql(false); - expect(isValueTypeNumber('rgba(252,171,48,0.62)')).to.eql(false); - }); - it('correctly recognizes a color value', function() { - expect(isValueTypeColor(1234)).to.eql(false); - expect(isValueTypeColor(['time'])).to.eql(false); - expect(isValueTypeColor(['clamp', ['get', 'attr'], -1, 1])).to.eql(false); - expect(isValueTypeColor(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(true); - expect(isValueTypeColor('yellow')).to.eql(true); - expect(isValueTypeColor('#113366')).to.eql(true); - expect(isValueTypeColor('rgba(252,171,48,0.62)')).to.eql(true); - expect(isValueTypeColor('abcd')).to.eql(false); - }); - it('correctly recognizes a string value', function() { - expect(isValueTypeString(1234)).to.eql(false); - expect(isValueTypeString(['time'])).to.eql(false); - expect(isValueTypeString(['clamp', ['get', 'attr'], -1, 1])).to.eql(false); - expect(isValueTypeString(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(false); - expect(isValueTypeString('yellow')).to.eql(true); - expect(isValueTypeString('#113366')).to.eql(true); - expect(isValueTypeString('rgba(252,171,48,0.62)')).to.eql(true); - expect(isValueTypeString('abcd')).to.eql(true); - }); - it('throws on an unsupported type', function(done) { - try { - isValueTypeColor(true); - } catch (e) { - done(); - } - done(true); - }); - }); - describe('getSymbolVertexShader', function() { it('generates a symbol vertex shader (with varying)', function() { const builder = new ShaderBuilder(); - builder.addVarying('v_opacity', 'float', formatNumber(0.4)); - builder.addVarying('v_test', 'vec3', formatArray([1, 2, 3])); - builder.setSizeExpression(`vec2(${formatNumber(6)})`); - builder.setSymbolOffsetExpression(formatArray([5, -7])); - builder.setColorExpression(formatColor([80, 0, 255, 1])); - builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); + builder.addVarying('v_opacity', 'float', numberToGlsl(0.4)); + builder.addVarying('v_test', 'vec3', arrayToGlsl([1, 2, 3])); + builder.setSizeExpression(`vec2(${numberToGlsl(6)})`); + builder.setSymbolOffsetExpression(arrayToGlsl([5, -7])); + builder.setColorExpression(colorToGlsl([80, 0, 255, 1])); + builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1])); expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float; uniform mat4 u_projectionMatrix; @@ -144,10 +49,10 @@ void main(void) { const builder = new ShaderBuilder(); builder.addUniform('float u_myUniform'); builder.addAttribute('vec2 a_myAttr'); - builder.setSizeExpression(`vec2(${formatNumber(6)})`); - builder.setSymbolOffsetExpression(formatArray([5, -7])); - builder.setColorExpression(formatColor([80, 0, 255, 1])); - builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); + builder.setSizeExpression(`vec2(${numberToGlsl(6)})`); + builder.setSymbolOffsetExpression(arrayToGlsl([5, -7])); + builder.setColorExpression(colorToGlsl([80, 0, 255, 1])); + builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1])); expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float; uniform mat4 u_projectionMatrix; @@ -181,10 +86,10 @@ void main(void) { }); it('generates a symbol vertex shader (with rotateWithView)', function() { const builder = new ShaderBuilder(); - builder.setSizeExpression(`vec2(${formatNumber(6)})`); - builder.setSymbolOffsetExpression(formatArray([5, -7])); - builder.setColorExpression(formatColor([80, 0, 255, 1])); - builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); + builder.setSizeExpression(`vec2(${numberToGlsl(6)})`); + builder.setSymbolOffsetExpression(arrayToGlsl([5, -7])); + builder.setColorExpression(colorToGlsl([80, 0, 255, 1])); + builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1])); builder.setSymbolRotateWithView(true); expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float; @@ -222,12 +127,12 @@ void main(void) { describe('getSymbolFragmentShader', function() { it('generates a symbol fragment shader (with varying)', function() { const builder = new ShaderBuilder(); - builder.addVarying('v_opacity', 'float', formatNumber(0.4)); - builder.addVarying('v_test', 'vec3', formatArray([1, 2, 3])); - builder.setSizeExpression(`vec2(${formatNumber(6)})`); - builder.setSymbolOffsetExpression(formatArray([5, -7])); - builder.setColorExpression(formatColor([80, 0, 255])); - builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); + builder.addVarying('v_opacity', 'float', numberToGlsl(0.4)); + builder.addVarying('v_test', 'vec3', arrayToGlsl([1, 2, 3])); + builder.setSizeExpression(`vec2(${numberToGlsl(6)})`); + builder.setSymbolOffsetExpression(arrayToGlsl([5, -7])); + builder.setColorExpression(colorToGlsl([80, 0, 255])); + builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1])); expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float; uniform float u_time; @@ -246,10 +151,10 @@ void main(void) { const builder = new ShaderBuilder(); builder.addUniform('float u_myUniform'); builder.addUniform('vec2 u_myUniform2'); - builder.setSizeExpression(`vec2(${formatNumber(6)})`); - builder.setSymbolOffsetExpression(formatArray([5, -7])); - builder.setColorExpression(formatColor([255, 255, 255, 1])); - builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); + builder.setSizeExpression(`vec2(${numberToGlsl(6)})`); + builder.setSymbolOffsetExpression(arrayToGlsl([5, -7])); + builder.setColorExpression(colorToGlsl([255, 255, 255, 1])); + builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1])); builder.setFragmentDiscardExpression('u_myUniform > 0.5'); expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float; @@ -267,154 +172,6 @@ void main(void) { }); }); - describe('check', function() { - - it('does not throw on valid expressions', function(done) { - check(1); - check('attr'); - check('rgba(12, 34, 56, 0.5)'); - check([255, 255, 255, 1]); - check([255, 255, 255]); - check(['get', 'myAttr']); - check(['var', 'myValue']); - check(['time']); - check(['+', ['*', ['get', 'size'], 0.001], 12]); - check(['/', ['-', ['get', 'size'], 0.001], 12]); - check(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20]); - check(['stretch', ['get', 'size'], 10, 100, 4, 8]); - check(['mod', ['pow', ['get', 'size'], 0.5], 12]); - check(['>', 10, ['get', 'attr4']]); - check(['>=', 10, ['get', 'attr4']]); - check(['<', 10, ['get', 'attr4']]); - check(['<=', 10, ['get', 'attr4']]); - check(['==', 10, ['get', 'attr4']]); - check(['between', ['get', 'attr4'], -4.0, 5.0]); - check(['!', ['get', 'attr4']]); - check(['interpolate', ['get', 'attr4'], 'green', '#3344FF']); - check(['interpolate', 0.2, [10, 20, 30], [255, 255, 255, 1]]); - done(); - }); - - it('throws on unsupported types for operators', function() { - let thrown = false; - try { - check(['var', 1234]); - } catch (e) { - thrown = true; - } - try { - check(['<', 0, 'aa']); - } catch (e) { - thrown = true; - } - try { - check(['+', true, ['get', 'attr']]); - } catch (e) { - thrown = true; - } - try { - check(['interpolate', ['get', 'attr4'], 1, '#3344FF']); - } catch (e) { - thrown = true; - } - expect(thrown).to.be(true); - }); - - it('throws with the wrong number of arguments', function() { - let thrown = false; - try { - check(['var', 1234, 456]); - } catch (e) { - thrown = true; - } - try { - check(['<', 4]); - } catch (e) { - thrown = true; - } - try { - check(['+']); - } catch (e) { - thrown = true; - } - expect(thrown).to.be(true); - }); - - it('throws on invalid expressions', function() { - let thrown = false; - try { - check(true); - } catch (e) { - thrown = true; - } - try { - check([123, 456]); - } catch (e) { - thrown = true; - } - try { - check(null); - } catch (e) { - thrown = true; - } - expect(thrown).to.be(true); - }); - - }); - - describe('parse', function() { - let attributes, prefix, variables, parseFn; - - beforeEach(function() { - attributes = []; - variables = []; - prefix = 'a_'; - parseFn = function(value, type) { - return parse(value, attributes, prefix, variables, type); - }; - }); - - it('parses expressions & literal values', function() { - expect(parseFn(1)).to.eql('1.0'); - expect(parseFn('a_random_string')).to.eql('"a_random_string"'); - expect(parseFn([255, 127.5, 63.75, 0.1])).to.eql('vec4(1.0, 0.5, 0.25, 0.1)'); - expect(parseFn([255, 127.5, 63.75])).to.eql('vec4(1.0, 0.5, 0.25, 1.0)'); - expect(parseFn(['get', 'myAttr'])).to.eql('a_myAttr'); - expect(parseFn(['var', 'myValue'])).to.eql('u_myValue'); - expect(parseFn(['time'])).to.eql('u_time'); - expect(parseFn(['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.0)'); - expect(parseFn(['/', ['-', ['get', 'size'], 20], 100])).to.eql('((a_size - 20.0) / 100.0)'); - expect(parseFn(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql('clamp(a_attr2, a_attr3, 20.0)'); - expect(parseFn(['stretch', ['get', 'size'], 10, 100, 4, 8])).to.eql('((clamp(a_size, 10.0, 100.0) - 10.0) * ((8.0 - 4.0) / (100.0 - 10.0)) + 4.0)'); - expect(parseFn(['pow', ['mod', ['time'], 10], 2])).to.eql('pow(mod(u_time, 10.0), 2.0)'); - expect(parseFn(['>', 10, ['get', 'attr4']])).to.eql('(10.0 > a_attr4 ? 1.0 : 0.0)'); - expect(parseFn(['>=', 10, ['get', 'attr4']])).to.eql('(10.0 >= a_attr4 ? 1.0 : 0.0)'); - expect(parseFn(['<', 10, ['get', 'attr4']])).to.eql('(10.0 < a_attr4 ? 1.0 : 0.0)'); - expect(parseFn(['<=', 10, ['get', 'attr4']])).to.eql('(10.0 <= a_attr4 ? 1.0 : 0.0)'); - expect(parseFn(['==', 10, ['get', 'attr4']])).to.eql('(10.0 == a_attr4 ? 1.0 : 0.0)'); - expect(parseFn(['between', ['get', 'attr4'], -4.0, 5.0])).to.eql('(a_attr4 >= -4.0 && a_attr4 <= 5.0 ? 1.0 : 0.0)'); - expect(parseFn(['!', ['get', 'attr4']])).to.eql('(a_attr4 > 0.0 ? 0.0 : 1.0)'); - expect(parseFn(['interpolate', ['get', 'attr4'], [255, 255, 255, 1], 'transparent'])).to.eql( - 'mix(vec4(1.0, 1.0, 1.0, 1.0), vec4(0.0, 0.0, 0.0, 0.0), a_attr4)'); - expect(attributes).to.eql(['myAttr', 'size', 'attr2', 'attr3', 'attr4']); - expect(variables).to.eql(['myValue']); - }); - - it('gives precedence to the string type unless asked otherwise', function() { - expect(parseFn('lightgreen')).to.eql('"lightgreen"'); - expect(parseFn('lightgreen', ValueTypes.COLOR)).to.eql('vec4(0.5647058823529412, 0.9333333333333333, 0.5647058823529412, 1.0)'); - }); - - it('does not register an attribute several times', function() { - parseFn(['get', 'myAttr']); - parseFn(['var', 'myVar']); - parseFn(['clamp', ['get', 'attr2'], ['get', 'attr2'], ['get', 'myAttr']]); - parseFn(['*', ['get', 'attr2'], ['var', 'myVar']]); - expect(attributes).to.eql(['myAttr', 'attr2']); - expect(variables).to.eql(['myVar']); - }); - }); - describe('parseSymbolStyle', function() { it('parses a style without expressions', function() { const result = parseLiteralStyle({ @@ -553,7 +310,7 @@ void main(void) { expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.0)'); expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)'); expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)'); - expect(result.builder.discardExpression).to.eql('(v_attr0 >= 0.0 && v_attr0 <= 10.0 ? 1.0 : 0.0) <= 0.0'); + expect(result.builder.discardExpression).to.eql('(v_attr0 >= 0.0 && v_attr0 <= 10.0)'); expect(result.builder.rotateWithView).to.eql(false); expect(result.attributes.length).to.eql(1); expect(result.attributes[0].name).to.eql('attr0');