diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js index e44e290b52..7699132a4a 100644 --- a/src/ol/webgl/ShaderBuilder.js +++ b/src/ol/webgl/ShaderBuilder.js @@ -141,3 +141,57 @@ void main(void) { return body; } + +/** + * Base type for values fed to operators; can be a number literal or the output of another operator + * @typedef {Array<*>|number} OperatorValue + */ + +/** + * 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)' + * + * The following operators can be used: + * * `['get', 'attributeName']` fetches a feature attribute (it will be prefixed by `a_` in the shader) + * * `['*', value1, value1]` multiplies value1 by value2 + * * `['+', value1, value1]` adds value1 and value2 + * * `['clamp', value1, value2, value3]` clamps value1 between values2 and value3 + * * `['stretch', value1, value2, value3, value4, value5]` maps value1 from [value2, value3] range to + * [value4, value5] range, clamping values along the way + * + * Values can either be literals (numbers) or another operator, as they will be evaluated recursively. + * + * Also takes in an array where new attributes will be pushed, so that the user of the `parse` function + * knows which attributes are expected to be available at evaluation time. + * + * A prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in + * the final assignment string. + * + * @param {OperatorValue} value Either literal or an operator. + * @param {Array} attributes Array containing the attribute names prefixed with `a_`; it + * it passed along recursively + * @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`). + * @returns {string} Assignment string. + */ +export function parse(value, attributes, attributePrefix) { + const v = value; + function p(value) { + return parse(value, attributes, attributePrefix); + } + if (Array.isArray(v)) { + switch (v[0]) { + case 'get': + if (attributes.indexOf(v[1]) === -1) { + attributes.push(v[1]); + } + return attributePrefix + v[1]; + case '*': return `(${p(v[1])} * ${p(v[2])})`; + case '+': return `(${p(v[1])} + ${p(v[2])})`; + case 'clamp': return `clamp(${p(v[1])}, ${p(v[2])}, ${p(v[3])})`; + case 'stretch': return `(clamp(${p(v[1])}, ${p(v[2])}, ${p(v[3])}) * ((${p(v[5])} - ${p(v[4])}) / (${p(v[3])} - ${p(v[2])})) + ${p(v[4])})`; + default: throw new Error('Unrecognized literal style expression: ' + JSON.stringify(value)); + } + } else { + return formatNumber(value); + } +} diff --git a/test/spec/ol/webgl/shaderbuilder.test.js b/test/spec/ol/webgl/shaderbuilder.test.js index 9c3a2cd9d8..a8bed4e62a 100644 --- a/test/spec/ol/webgl/shaderbuilder.test.js +++ b/test/spec/ol/webgl/shaderbuilder.test.js @@ -2,7 +2,7 @@ import { getSymbolVertexShader, formatNumber, getSymbolFragmentShader, - formatColor, formatArray + formatColor, formatArray, parse } from '../../../../src/ol/webgl/ShaderBuilder.js'; describe('ol.webgl.ShaderBuilder', function() { @@ -196,4 +196,31 @@ void main(void) { }); }); + describe('parse', function() { + let attributes, prefix, parseFn; + + beforeEach(function() { + attributes = []; + prefix = 'a_'; + parseFn = function(value) { + return parse(value, attributes, prefix); + }; + }); + + it('parses expressions & literal values', function() { + expect(parseFn(1)).to.eql('1.0'); + expect(parseFn(['get', 'myAttr'])).to.eql('a_myAttr'); + expect(parseFn(['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.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) * ((8.0 - 4.0) / (100.0 - 10.0)) + 4.0)'); + expect(attributes).to.eql(['myAttr', 'size', 'attr2', 'attr3']); + }); + + it('does not register an attribute several times', function() { + parseFn(['get', 'myAttr']); + parseFn(['clamp', ['get', 'attr2'], ['get', 'attr2'], ['get', 'myAttr']]); + expect(attributes).to.eql(['myAttr', 'attr2']); + }); + }); + });