From 23826825a6900c5451528707a86fe0a69a8b614a Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 27 Nov 2023 13:29:37 +0100 Subject: [PATCH] wip better support for dynamic name expressions in get/var --- src/ol/expr/expression.js | 16 + src/ol/expr/gpu.js | 363 +++++++++++++----- src/ol/webgl/styleparser.js | 13 +- .../browser/spec/ol/webgl/styleparser.test.js | 4 +- test/node/ol/expr/gpu.test.js | 67 +++- 5 files changed, 338 insertions(+), 125 deletions(-) diff --git a/src/ol/expr/expression.js b/src/ol/expr/expression.js index 9161ea820b9..c5137c217ee 100644 --- a/src/ol/expr/expression.js +++ b/src/ol/expr/expression.js @@ -197,6 +197,20 @@ export class LiteralExpression { } } +/** + * @param {Expression} expr Expression + * @return {boolean} Whether the expression relies on properties + */ +function checkReliesOnProperties(expr) { + if (expr instanceof LiteralExpression) { + return false; + } + if (expr.reliesOnProperties) { + return true; + } + return expr.args.some(checkReliesOnProperties); +} + export class CallExpression { /** * @param {number} type The return type. @@ -207,6 +221,8 @@ export class CallExpression { this.type = type; this.operator = operator; this.args = args; + this.reliesOnProperties = + operator === 'get' || args.some(checkReliesOnProperties); } } diff --git a/src/ol/expr/gpu.js b/src/ol/expr/gpu.js index 4234a33338a..788386ab225 100644 --- a/src/ol/expr/gpu.js +++ b/src/ol/expr/gpu.js @@ -6,6 +6,7 @@ import { BooleanType, CallExpression, ColorType, + LiteralExpression, NoneType, NumberArrayType, NumberType, @@ -19,12 +20,13 @@ import { typeName, } from './expression.js'; import {GLSL_UNDEFINED_VALUE} from '../render/webgl/constants.js'; -import {Uniforms} from '../renderer/webgl/TileLayer.js'; -import {asArray} from '../color.js'; import { + UNKNOWN_VALUE, compileExpression as compileExpressionCpu, newEvaluationContext as newEvaluationContextCpu, } from './cpu.js'; +import {Uniforms} from '../renderer/webgl/TileLayer.js'; +import {asArray} from '../color.js'; /** * @param {string} operator Operator @@ -138,15 +140,34 @@ export function uniformNameForVariable(variableName) { } /** - * @typedef {import('./expression.js').ParsingContext} ParsingContext + * Get the attribute name given a property name. + * @param {string} propertyName The property name. + * @param {boolean} inFragmentShader Whether the compilation targets a fragment shader + * @return {string} The attribute name. */ +export function attributeNameForProperty(propertyName, inFragmentShader) { + const prefix = inFragmentShader ? 'v_prop_' : 'a_prop_'; + return prefix + propertyName; +} + /** - * - * @typedef {import("./expression.js").Expression} Expression + * see https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript + * @param {Object|string} input The hash input, either an object or string + * @return {string} Hash + */ +export function computeHash(input) { + const hash = JSON.stringify(input) + .split('') + .reduce((prev, curr) => (prev << 5) - prev + curr.charCodeAt(0), 0); + return (hash >>> 0).toString(); +} + +/** + * @typedef {import('./expression.js').ParsingContext} ParsingContext */ /** * - * @typedef {import("./expression.js").LiteralExpression} LiteralExpression + * @typedef {import("./expression.js").Expression} Expression */ /** @@ -254,14 +275,20 @@ function createCompiler(output) { * Will compile an expression on the CPU and return a value right away * @param {Expression} expression The expression * @param {number} expectedType Expected return type + * @param {Object} [properties] Properties used for evaluation + * @param {Object} [variables] Variables used for evaluation * @return {import("./expression.js").LiteralValue} Return value */ -function evaluateOnCpu(expression, expectedType) { +function evaluateOnCpu(expression, expectedType, properties, variables) { const parsingContext = newParsingContext(); const compiled = compileExpressionCpu(expression, parsingContext); const evalContext = newEvaluationContextCpu(); - evalContext.properties = {}; - evalContext.resolution = 1; + if (properties) { + evalContext.properties = properties; + } + if (variables) { + evalContext.variables = variables; + } return compiled(evalContext); } @@ -269,49 +296,8 @@ function evaluateOnCpu(expression, expectedType) { * @type {Object} */ const compilers = { - [Ops.Get]: (context, expression, type) => { - const nameArg = expression.args[0]; - const propName = /** @type {string} */ (evaluateOnCpu(nameArg, StringType)); - const isExisting = propName in context.properties; - if (!isExisting) { - /** @type {CompilationContextPropertyEvaluator} */ - let evaluator; - if (expression.type === StringType) { - evaluator = (feature) => { - const value = feature.get(propName); - if (value === undefined || value === null) { - return GLSL_UNDEFINED_VALUE; - } - return getStringNumberEquivalent(value); - }; - } else if (expression.type === ColorType) { - evaluator = (feature) => { - const value = feature.get(propName); - if (value === undefined || value === null) { - return GLSL_UNDEFINED_VALUE; - } - return packColor([...asArray(value)]); - }; - } else if (expression.type === BooleanType) { - evaluator = (feature) => { - const value = feature.get(propName); - if (value === undefined || value === null) { - return GLSL_UNDEFINED_VALUE; - } - return value ? 1.0 : 0.0; - }; - } else { - evaluator = (feature) => feature.get(propName) ?? GLSL_UNDEFINED_VALUE; - } - context.properties[propName] = { - name: propName, - type: expression.type, - evaluator, - }; - } - const prefix = context.inFragmentShader ? 'v_prop_' : 'a_prop_'; - return prefix + propName; - }, + [Ops.Get]: compileGet, + [Ops.Var]: compileVar, [Ops.GeometryType]: (context, expression, type) => { const propName = 'geometryType'; const isExisting = propName in context.properties; @@ -327,51 +313,6 @@ const compilers = { const prefix = context.inFragmentShader ? 'v_prop_' : 'a_prop_'; return prefix + propName; }, - [Ops.Var]: (context, expression, type) => { - const nameArg = expression.args[0]; - const varName = /** @type {string} */ (evaluateOnCpu(nameArg, StringType)); - const isExisting = varName in context.variables; - if (!isExisting) { - /** @type {CompilationContextVariableEvaluator} */ - let evaluator; - if (expression.type === StringType) { - evaluator = (variables) => { - const value = variables[varName]; - if (value === undefined || value === null) { - return GLSL_UNDEFINED_VALUE; - } - return getStringNumberEquivalent(/** @type {string} */ value); - }; - } else if (expression.type === ColorType) { - evaluator = (variables) => { - const value = variables[varName]; - if (value === undefined || value === null) { - return GLSL_UNDEFINED_VALUE; - } - return packColor([ - ...asArray(/** @type {string|Array} */ value), - ]); - }; - } else if (expression.type === BooleanType) { - evaluator = (variables) => { - const value = variables[varName]; - if (value === undefined || value === null) { - return GLSL_UNDEFINED_VALUE; - } - return /** @type {boolean} */ (value) ? 1.0 : 0.0; - }; - } else { - evaluator = (variables) => - /** @type {number} */ (variables[varName]) ?? GLSL_UNDEFINED_VALUE; - } - context.variables[varName] = { - name: varName, - type: expression.type, - evaluator, - }; - } - return uniformNameForVariable(varName); - }, [Ops.Resolution]: () => 'u_resolution', [Ops.Zoom]: () => 'u_zoom', [Ops.Time]: () => 'u_time', @@ -428,7 +369,9 @@ const compilers = { const result = /** @type {string} */ ( evaluateOnCpu(expression, StringType) ); - return stringToGlsl(result); + return result === UNKNOWN_VALUE + ? numberToGlsl(GLSL_UNDEFINED_VALUE) + : stringToGlsl(result); }, [Ops.Match]: createCompiler((compiledArgs) => { const input = compiledArgs[0]; @@ -562,6 +505,230 @@ ${ifBlocks} // Ops.String }; +/** + * @type {Compiler} + */ +function compileGet(context, expression, type) { + const nameArg = expression.args[0]; + const isNameLiteral = nameArg instanceof LiteralExpression; + const propName = isNameLiteral + ? /** @type {string} */ (nameArg.value) + : `get${computeHash(nameArg)}`; + + // property is already defined + if (propName in context.properties) { + return attributeNameForProperty(propName, context.inFragmentShader); + } + + /** @type {CompilationContextPropertyEvaluator} */ + let evaluator; + if (expression.type === StringType) { + evaluator = (feature) => { + const name = isNameLiteral + ? propName + : /** @type {string} */ ( + evaluateOnCpu(nameArg, StringType, feature.getPropertiesInternal()) + ); + const value = feature.get(name); + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return getStringNumberEquivalent(value); + }; + } else if (expression.type === ColorType) { + evaluator = (feature) => { + const name = isNameLiteral + ? propName + : /** @type {string} */ ( + evaluateOnCpu(nameArg, StringType, feature.getPropertiesInternal()) + ); + const value = feature.get(name); + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return packColor([...asArray(value)]); + }; + } else if (expression.type === BooleanType) { + evaluator = (feature) => { + const name = isNameLiteral + ? propName + : /** @type {string} */ ( + evaluateOnCpu(nameArg, StringType, feature.getPropertiesInternal()) + ); + const value = feature.get(name); + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return value ? 1.0 : 0.0; + }; + } else { + evaluator = (feature) => { + const name = isNameLiteral + ? propName + : /** @type {string} */ ( + evaluateOnCpu(nameArg, StringType, feature.getPropertiesInternal()) + ); + return feature.get(name) ?? GLSL_UNDEFINED_VALUE; + }; + } + context.properties[propName] = { + name: propName, + type: expression.type, + evaluator, + }; + return attributeNameForProperty(propName, context.inFragmentShader); +} + +/** + * @type {Compiler} + */ +function compileVar(context, expression, type) { + const nameArg = expression.args[0]; + const isNameLiteral = nameArg instanceof LiteralExpression; + const varName = isNameLiteral + ? /** @type {string} */ (nameArg.value) + : `var${computeHash(nameArg)}`; + + // the variable is already defined + if (varName in context.variables) { + return uniformNameForVariable(varName); + } + + if (!isNameLiteral && nameArg.reliesOnProperties) { + const variables = context?.style?.variables || undefined; + /** @type {CompilationContextPropertyEvaluator} */ + let evaluator; + if (expression.type === StringType) { + evaluator = (feature) => { + const name = /** @type {string} */ ( + evaluateOnCpu( + nameArg, + StringType, + feature.getPropertiesInternal(), + variables + ) + ); + const value = variables?.[name]; + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return getStringNumberEquivalent(/** @type {string} */ (value)); + }; + } else if (expression.type === ColorType) { + evaluator = (feature) => { + const name = /** @type {string} */ ( + evaluateOnCpu( + nameArg, + StringType, + feature.getPropertiesInternal(), + variables + ) + ); + const value = variables?.[name]; + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return packColor([ + ...asArray(/** @type {string|import("../color.js").Color} */ (value)), + ]); + }; + } else if (expression.type === BooleanType) { + evaluator = (feature) => { + const name = /** @type {string} */ ( + evaluateOnCpu( + nameArg, + StringType, + feature.getPropertiesInternal(), + variables + ) + ); + const value = variables?.[name]; + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return value ? 1.0 : 0.0; + }; + } else { + evaluator = (feature) => { + const name = /** @type {string} */ ( + evaluateOnCpu( + nameArg, + StringType, + feature.getPropertiesInternal(), + variables + ) + ); + return variables?.[name] ?? GLSL_UNDEFINED_VALUE; + }; + } + context.properties[varName] = { + name: varName, + type: expression.type, + evaluator, + }; + return attributeNameForProperty(varName, context.inFragmentShader); + } + + /** @type {CompilationContextVariableEvaluator} */ + let evaluator; + if (expression.type === StringType) { + evaluator = (variables) => { + const name = isNameLiteral + ? varName + : /** @type {string} */ ( + evaluateOnCpu(nameArg, StringType, undefined, variables) + ); + const value = variables[name]; + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return getStringNumberEquivalent(/** @type {string} */ value); + }; + } else if (expression.type === ColorType) { + evaluator = (variables) => { + const name = isNameLiteral + ? varName + : /** @type {string} */ ( + evaluateOnCpu(nameArg, StringType, undefined, variables) + ); + const value = variables[name]; + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return packColor([...asArray(/** @type {string|Array} */ value)]); + }; + } else if (expression.type === BooleanType) { + evaluator = (variables) => { + const name = isNameLiteral + ? varName + : /** @type {string} */ ( + evaluateOnCpu(nameArg, StringType, undefined, variables) + ); + const value = variables[name]; + if (value === undefined || value === null) { + return GLSL_UNDEFINED_VALUE; + } + return /** @type {boolean} */ (value) ? 1.0 : 0.0; + }; + } else { + evaluator = (variables) => { + const name = isNameLiteral + ? varName + : /** @type {string} */ ( + evaluateOnCpu(nameArg, StringType, undefined, variables) + ); + return /** @type {number} */ (variables[name]) ?? GLSL_UNDEFINED_VALUE; + }; + } + context.variables[varName] = { + name: varName, + type: expression.type, + evaluator, + }; + + return uniformNameForVariable(varName); +} + /** * @param {Expression} expression The expression. * @param {number} returnType The expected return type. diff --git a/src/ol/webgl/styleparser.js b/src/ol/webgl/styleparser.js index b473a90e7c5..4442b8839bc 100644 --- a/src/ol/webgl/styleparser.js +++ b/src/ol/webgl/styleparser.js @@ -15,6 +15,7 @@ import { UNPACK_COLOR_FN, arrayToGlsl, buildExpression, + computeHash, stringToGlsl, uniformNameForVariable, } from '../expr/gpu.js'; @@ -64,18 +65,6 @@ function getGlslTypeFromType(type) { return 'float'; } -/** - * see https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript - * @param {Object|string} input The hash input, either an object or string - * @return {string} Hash - */ -export function computeHash(input) { - const hash = JSON.stringify(input) - .split('') - .reduce((prev, curr) => (prev << 5) - prev + curr.charCodeAt(0), 0); - return (hash >>> 0).toString(); -} - /** * @param {import("../style/webgl.js").WebGLStyle} style Style * @param {ShaderBuilder} builder Shader builder diff --git a/test/browser/spec/ol/webgl/styleparser.test.js b/test/browser/spec/ol/webgl/styleparser.test.js index 9892ff0bbcc..9a07c73f631 100644 --- a/test/browser/spec/ol/webgl/styleparser.test.js +++ b/test/browser/spec/ol/webgl/styleparser.test.js @@ -2,13 +2,11 @@ import Feature from '../../../../../src/ol/Feature.js'; import {asArray} from '../../../../../src/ol/color.js'; import { computeHash, - parseLiteralStyle, -} from '../../../../../src/ol/webgl/styleparser.js'; -import { packColor, stringToGlsl, uniformNameForVariable, } from '../../../../../src/ol/expr/gpu.js'; +import {parseLiteralStyle} from '../../../../../src/ol/webgl/styleparser.js'; describe('ol.webgl.styleparser', () => { describe('parseLiteralStyle', () => { diff --git a/test/node/ol/expr/gpu.test.js b/test/node/ol/expr/gpu.test.js index 15689999a40..341975717f1 100644 --- a/test/node/ol/expr/gpu.test.js +++ b/test/node/ol/expr/gpu.test.js @@ -152,14 +152,27 @@ describe('ol/expr/gpu.js', () => { }, }, { - name: 'get (dynamic property name)', + name: 'get (property name as expression)', type: AnyType, - expression: ['get', ['concat', 'hello', 'World']], - expected: 'a_prop_helloWorld', + expression: ['get', ['concat', 'hello-', 'world']], + expected: 'a_prop_get3868214767', contextAssertion(context) { - expect(context.properties).to.have.property('helloWorld'); - const evaluator = context.properties.helloWorld.evaluator; - expect(evaluator(new Feature({helloWorld: 123}))).to.equal(123); + expect(context.properties).to.have.property('get3868214767'); + const evaluator = context.properties['get3868214767'].evaluator; + expect(evaluator(new Feature({'hello-world': 123}))).to.equal(123); + }, + }, + { + name: 'get (property name as expression relying on properties)', + type: AnyType, + expression: ['get', ['concat', 'hello-', ['get', 'type']]], + expected: 'a_prop_get4130076217', + contextAssertion(context) { + expect(context.properties).to.have.property('get4130076217'); + const evaluator = context.properties['get4130076217'].evaluator; + expect( + evaluator(new Feature({type: 'world', 'hello-world': 123})) + ).to.equal(123); }, }, { @@ -191,14 +204,32 @@ describe('ol/expr/gpu.js', () => { }, }, { - name: 'var (dynamic variable name)', + name: 'var (variable name as expression)', type: AnyType, - expression: ['var', ['concat', 'hello', 'World']], - expected: 'u_var_helloWorld', + expression: ['var', ['concat', 'hello-', 'world']], + expected: 'u_var_var3868214767', + contextAssertion(context) { + expect(context.variables).to.have.property('var3868214767'); + const evaluator = context.variables['var3868214767'].evaluator; + expect(evaluator({'hello-world': 123})).to.equal(123); + }, + }, + { + name: 'var (variable name as expression relying on properties)', + type: AnyType, + expression: ['var', ['concat', 'hello-', ['get', 'type']]], + expected: 'a_prop_var4130076217', + context: { + style: { + variables: { + 'hello-world': 123, + }, + }, + }, contextAssertion(context) { - expect(context.variables).to.have.property('helloWorld'); - const evaluator = context.variables.helloWorld.evaluator; - expect(evaluator({helloWorld: 123})).to.equal(123); + expect(context.properties).to.have.property('var4130076217'); + const evaluator = context.properties['var4130076217'].evaluator; + expect(evaluator(new Feature({type: 'world'}))).to.equal(123); }, }, { @@ -357,6 +388,18 @@ describe('ol/expr/gpu.js', () => { expression: ['concat', 'hello', 'World', '!'], expected: getStringNumberEquivalent('helloWorld!'), }, + { + name: 'concat (rely on variables, return value unknown)', + type: StringType, + expression: ['concat', 'hello-', ['var', 'type']], + expected: GLSL_UNDEFINED_VALUE, + }, + { + name: 'concat (rely on properties, return value unknown)', + type: StringType, + expression: ['concat', 'hello-', ['get', 'type']], + expected: GLSL_UNDEFINED_VALUE, + }, { name: 'greater than', type: AnyType,