UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

737 lines (695 loc) 23.7 kB
/** * The following expressions are available to use in most parameters: * * ## Variables * * - `$F`: current frame * - `$T`: current time * - `$OS`: current node name * - `$CH`: current param name * - `$CEX`: input centroid x component * - `$CEY`: input centroid y component * - `$CEZ`: input centroid z component * * Those variables are aliases to the javascript math module: * * - `$E` * - `$LN2` * - `$LN10` * - `$LOG10E` * - `$LOG2E` * - `$PI` * - `$SQRT1_2` * - `$SQRT2` * * In some parameters which can evaluate per entity (which means per point or per object), * it is also possible to use variables to access attributes: * * - `@ptnum` will evaluate to the current point index * - `@vtxnum` will evaluate to the current vertex index * - `@primnum` will evaluate to the current primitive index * - `@objnum` will evaluate to the current object index * - And you can also refer to any existing attribute, **using @ following by the attribute name**. * * For instance: * * - `@P.x` evaluates to the **x** component of the position. * - `@P.y` evaluates to the **y** component of the position. * - `@P.z` evaluates to the **z** component of the position. * - `@N.x` evaluates to the **x** component of the normal. * - `@N.y` evaluates to the **y** component of the normal. * - `@N.z` evaluates to the **z** component of the normal. * - `@Cd.x` evaluates to the **x** component of the color. * - `@Cd.y` evaluates to the **y** component of the color. * - `@Cd.z` evaluates to the **z** component of the color. * - `@uv.x` evaluates to the **x** component of the uv. * - `@uv.y` evaluates to the **y** component of the uv. * * Using the attribCreate, point or normal SOPs, you can mix and match them. * For instance, if in the point SOP, you set to the x component `@uv.x` and y component `@uv.y` * then the points will be transformed to look like in UV space. * * Another common setup is to use an attribute create to add an attribute `id`, with `@ptnum`. * This way, every point will have a unique id. * You can then use this id in the following nodes, or even in a material. * * Instead of having an attribute id that goes from 0 to the number of points in your geometry, * you can also create one that goes from 0 to 1, using `@ptnum / (pointsCount(0)-1)` * * * ## Math expressions * * The following are native javascript functions: * * - abs * - acos * - acosh * - asin * - asinh * - atan * - atan2 * - atanh * - ceil * - cos * - cosh * - exp * - expm1 * - floor * - log * - log1p * - log2 * - log10 * - max * - min * - pow * - random (which aliases to Math.rand()) * - round * - sign * - sin * - sinh * - sqrt * - tan * - tanh * * If you are targetting ES6 (available in modern browsers), you can also have: * * - cbrt * - hypot * - log10 * - trunc * * The following are aliases from the [Polygonjs CoreMath](https://github.com/polygonjs/polygonjs-engine/blob/master/src/core/math/_Module.ts) module: * * - clamp * - degToRad * - fit * - fit01 * - fitClamp * - fract * - mix * - radToDeg * - rand * * And the following are alias to the [Polygonjs Easing](https://github.com/polygonjs/polygonjs-engine/blob/master/src/core/math/Easing.ts) module: * * - `easeI2( number )`, is a shortcut for `ease_i( number, 2 )` * - `easeO2( number )`, is a shortcut for `ease_o( number, 2 )` * - `easeIO2( number )`, is a shortcut for `ease_io( number, 2 )` * - `easeI3( number )`, is a shortcut for `ease_i( number, 3 )` * - `easeO3( number )`, is a shortcut for `ease_o( number, 3 )` * - `easeIO3( number )`, is a shortcut for `ease_io( number, 3 )` * - `easeI4( number )`, is a shortcut for `ease_i( number, 4 )` * - `easeO4( number )`, is a shortcut for `ease_o( number, 4 )` * - `easeIO4( number )`, is a shortcut for `ease_io( number, 4 )` * - `easeSinI( number )` * - `easeSinO( number )` * - `easeSinIO( number )` * - `easeElasticI( number )` * - `easeElasticO( number )` * - `easeElasticIO( number )` * * * ## String expressions: * * - precision (alias to the [CoreString](https://github.com/polygonjs/polygonjs-engine/blob/master/src/core/String.ts) module precision method) * - [strCharsCount](/docs/expressions/strCharsCount) * - [strConcat](/docs/expressions/strConcat) * - [strIndex](/docs/expressions/strIndex) * - [strSub](/docs/expressions/strSub) * * */ import {Vector4} from 'three'; import {BaseParamType} from '../../params/_Base'; import {CoreGraphNode} from '../../../core/graph/CoreGraphNode'; import {ParsedTree} from './ParsedTree'; import {LiteralConstructsController, LiteralConstructMethod} from '../LiteralConstructsController'; import {BaseMethod} from '../methods/_Base'; import {Attribute, CoreAttribute} from '../../../core/geometry/Attribute'; import {BaseTraverser} from './_Base'; import {MethodDependency} from '../MethodDependency'; import { VAR_ENTITY, VAR_ENTITIES, FUNC_GET_ENTITIES_ATTRIBUTE, FUNC_GET_ENTITY_ATTRIBUTE_VALUE, FUNC_GET_ENTITY_ATTRIBUTE_VALUE_FUNC, } from '../Common'; import {AttributeRequirementsController} from '../AttributeRequirementsController'; import {CoreMath} from '../../../core/math/_Module'; import {CoreString} from '../../../core/String'; import {Poly} from '../../Poly'; import {CoreType, isString, isArray, isVector, isColor} from '../../../core/Type'; import {PolyDictionary} from '../../../types/GlobalTypes'; import {ThreejsPoint} from '../../../core/geometry/modules/three/ThreejsPoint'; import {corePointClassFactory} from '../../../core/geometry/CoreObjectFactory'; import {CoreEntity} from '../../../core/geometry/CoreEntity'; import jsep from 'jsep'; import {VARIABLE_PREFIX} from './_Base'; const QUOTE = "'"; const ARGUMENTS_SEPARATOR = ', '; const ATTRIBUTE_PREFIX = '@'; // import {Vector3} from 'three' type LiteralConstructDictionary = PolyDictionary<LiteralConstructMethod>; type AnyDictionary = PolyDictionary<any>; const NATIVE_MATH_METHODS = [ 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'cos', 'cosh', 'exp', 'expm1', 'floor', 'log', 'log1p', 'log2', 'log10', 'max', 'min', 'pow', 'round', 'sign', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', ]; const NATIVE_ES6_MATH_METHODS = ['cbrt', 'hypot', 'log10', 'trunc']; const NATIVE_MATH_METHODS_RENAMED: AnyDictionary = { math_random: 'random', }; const CORE_MATH_METHODS = ['fit', 'fit01', 'fract', 'deg2rad', 'rad2deg', 'rand', 'clamp']; import {Easing} from '../../../core/math/Easing'; const EASING_METHODS = Object.keys(Easing); const CORE_STRING_METHODS = ['precision']; const NATIVE_MATH_CONSTANTS = ['E', 'LN2', 'LN10', 'LOG10E', 'LOG2E', 'PI', 'SQRT1_2', 'SQRT2']; const DIRECT_EXPRESSION_FUNCTIONS: AnyDictionary = {}; NATIVE_MATH_METHODS.forEach((name) => { DIRECT_EXPRESSION_FUNCTIONS[name] = `Math.${name}`; }); NATIVE_ES6_MATH_METHODS.forEach((name) => { DIRECT_EXPRESSION_FUNCTIONS[name] = `Math.${name}`; }); Object.keys(NATIVE_MATH_METHODS_RENAMED).forEach((name) => { const remaped = NATIVE_MATH_METHODS_RENAMED[name]; DIRECT_EXPRESSION_FUNCTIONS[name] = `Math.${remaped}`; }); CORE_MATH_METHODS.forEach((name) => { DIRECT_EXPRESSION_FUNCTIONS[name] = `Core.Math.${name}`; }); EASING_METHODS.forEach((name) => { DIRECT_EXPRESSION_FUNCTIONS[name] = `Core.Math.Easing.${name}`; }); CORE_STRING_METHODS.forEach((name) => { DIRECT_EXPRESSION_FUNCTIONS[name] = `Core.String.${name}`; }); const LITERAL_CONSTRUCT: LiteralConstructDictionary = { if: LiteralConstructsController.if, }; const GLOBAL_CONSTANTS: PolyDictionary<string> = {}; NATIVE_MATH_CONSTANTS.forEach((name) => { GLOBAL_CONSTANTS[name] = `Math.${name}`; }); const PROPERTY_OFFSETS: AnyDictionary = { x: 0, y: 1, z: 2, w: 3, r: 0, g: 1, b: 2, }; type MethodArgumentFunction = () => string | number; const Core = { Math: CoreMath, String: CoreString, }; function getEntitiesAttributes(entities: CoreEntity[], attribName: string) { const firstEntity = entities[0]; if (firstEntity && firstEntity instanceof ThreejsPoint) { return firstEntity.attribute(attribName); } else { return entities.map((e) => e.attribValue(attribName, new Vector4())); } } type PropertyOffset = 0 | 1 | 2 | 3; type VectorProperty = 'x' | 'y' | 'z' | 'w'; type ColorProperty = 'r' | 'g' | 'b'; function getCorePointAttribValue( entity: ThreejsPoint, attribName: string, array: number[], attributeSize: number, propertyOffset: PropertyOffset ) { return array[entity.index() * attributeSize + propertyOffset]; } const VECTOR_PROPERTY_NAME_BY_OFFSET: Record<PropertyOffset, VectorProperty> = { 0: 'x', 1: 'y', 2: 'z', 3: 'w', }; const COLOR_PROPERTY_NAME_BY_OFFSET: Record<PropertyOffset, ColorProperty> = { 0: 'r', 1: 'g', 2: 'b', 3: 'r', }; const _target = new Vector4(); function getCoreEntityAttribValue( entity: CoreEntity, attribName: string, array: number[], attributeSize: number, propertyOffset: PropertyOffset ) { const value = entity.attribValue(attribName, _target); if (isArray(value)) { return value[propertyOffset]; } if (isVector(value)) { return (value as Vector4)[VECTOR_PROPERTY_NAME_BY_OFFSET[propertyOffset]]; } if (isColor(value)) { return value[COLOR_PROPERTY_NAME_BY_OFFSET[propertyOffset]]; } return value; } function getCoreEntityAttribValueFunc(entity: CoreEntity) { if (entity instanceof ThreejsPoint) { return getCorePointAttribValue; } return getCoreEntityAttribValue; } const FUNCTION_ARGS_DICT = { corePointClassFactory, ThreejsPoint, Core, CoreType, [FUNC_GET_ENTITIES_ATTRIBUTE]: getEntitiesAttributes, [FUNC_GET_ENTITY_ATTRIBUTE_VALUE_FUNC]: getCoreEntityAttribValueFunc, }; const FUNCTION_ARG_NAMES = Object.keys(FUNCTION_ARGS_DICT); const FUNCTION_ARGS = FUNCTION_ARG_NAMES.map((argName) => (FUNCTION_ARGS_DICT as any)[argName]); export class FunctionGenerator extends BaseTraverser { private _entitiesDependent: boolean = false; private function: Function | undefined; private _attribute_requirements_controller = new AttributeRequirementsController(); private function_main_string: string | undefined; private methods: BaseMethod[] = []; private method_index: number = -1; public methodDependencies: MethodDependency[] = []; public immutableDependencies: CoreGraphNode[] = []; // public jsep_dependencies: JsepDependency[] = [] // public jsep_nodes_by_missing_paths: JsepsByString = {} // private string_generator: ExpressionStringGenerator = new ExpressionStringGenerator() constructor(public override param: BaseParamType) { super(param); } entitiesDependent() { return this._entitiesDependent; } public parseTree(parsedTree: ParsedTree) { this.reset(); if (!parsedTree.errorMessage()) { try { // this.function_pre_entities_loop_lines = []; this._attribute_requirements_controller = new AttributeRequirementsController(); // this.function_pre_body = '' const node = parsedTree.node(); if (node) { const function_main_string = this.traverse_node(node); if (function_main_string && !this.isErrored()) { this.function_main_string = function_main_string; } } else { console.warn('no parsedTree.node'); } } catch (e) { console.warn(`error in expression for param ${this.param.path()}`); console.warn(e); } if (this.function_main_string) { try { const body = this._functionBody(); this.function = new Function( ...FUNCTION_ARG_NAMES, 'param', 'methods', '_set_error_from_error', ` try { ${body} } catch(e) { _set_error_from_error(e) return null; }` ); } catch (e) { console.warn(e); this.setError('cannot generate function'); } } else { this.setError('cannot generate function body'); } } else { this.setError('cannot parse expression'); } } override reset() { super.reset(); this.function_main_string = undefined; this.methods = []; this.method_index = -1; this.function = undefined; this._entitiesDependent = false; this.methodDependencies = []; this.immutableDependencies = []; } private _functionBody() { // const entitiesDependent = this.param.options.isExpressionForEntities() const entitiesDependent = this._entitiesDependent; if (entitiesDependent) { return ` const ${VAR_ENTITIES} = param.expressionController.entities(); if(${VAR_ENTITIES} && ${VAR_ENTITIES}.length > 0){ return new Promise( async (resolve, reject)=>{ try { const entityCallback = param.expressionController.entityCallback(); // assign_attributes_lines ${this._attribute_requirements_controller.assignAttributesLines()} // check if attributes are present if( ${this._attribute_requirements_controller.attributePresenceCheckLine()} ){ // assign function const ${FUNC_GET_ENTITY_ATTRIBUTE_VALUE} = ${FUNC_GET_ENTITY_ATTRIBUTE_VALUE_FUNC}(entities[0]); // assign_arrays_lines ${this._attribute_requirements_controller.assignArraysLines()} for(const ${VAR_ENTITY} of ${VAR_ENTITIES}){ result = ${this.function_main_string}; entityCallback(${VAR_ENTITY}, result); } resolve() } else { const missingAttributes = ${this._attribute_requirements_controller.missingAttributesLine()}().join(', '); const error = new Error('attribute ' + missingAttributes + ' not found') _set_error_from_error(error) reject(error) } }catch(e){ _set_error_from_error(e) reject(e) } }) } return []`; } else { return ` return new Promise( async (resolve, reject)=>{ try { const value = ${this.function_main_string} resolve(value) } catch(e) { _set_error_from_error(e) reject() } }) `; } } evalAllowed(): boolean { return this.function != null; } evalFunction() { if (this.function) { this.clearError(); const result = this.function(...FUNCTION_ARGS, this.param, this.methods, this._set_error_from_error_bound); return result; } } // // // TRAVERSE METHODS // // protected traverse_CallExpression(node: jsep.CallExpression): string | undefined { const methodArguments = node.arguments.map((arg) => { return this.traverse_node(arg); }); const callee = node.callee as jsep.Identifier; const method_name = callee.name; if (method_name) { // literal construct (if...) const literal_contruct = LITERAL_CONSTRUCT[method_name]; if (literal_contruct) { return literal_contruct(methodArguments); } // direct expressions (Math.floor, Math.sin...) const arguments_joined = `${methodArguments.join(ARGUMENTS_SEPARATOR)}`; const direct_function_name = DIRECT_EXPRESSION_FUNCTIONS[method_name]; if (direct_function_name) { return `${direct_function_name}(${arguments_joined})`; } // indirect methods (pointsCount, asset...) const expressionRegister = Poly.expressionsRegister; const indirect_method = expressionRegister.getMethod(method_name); if (indirect_method) { const pathNode = node.arguments[0]; // const path_argument = this.string_generator.traverse_node(path_node) const functionString = `return ${methodArguments[0]}`; let pathArgumentFunction: MethodArgumentFunction | undefined; let pathArgument: string | number | undefined; try { pathArgumentFunction = new Function(functionString) as MethodArgumentFunction; pathArgument = pathArgumentFunction(); } catch { // path_argument_function = new AsyncFunction(function_string) // it looks like if the input contains an await, // it is because it has been generated by another indirect function. // This means that the dependencies have been generated already // so we may not need to do it now } this._createMethodAndDependencies(method_name, pathArgument, pathNode); return `(await methods[${this.method_index}].processArguments([${arguments_joined}]))`; } else { const available_methods = expressionRegister.availableMethods().join(', '); const message = `method not found (${method_name}), available methods are: ${available_methods}`; Poly.warn(message); } } this.setError(`unknown method: ${method_name}`); } protected override traverse_BinaryExpression(node: jsep.BinaryExpression): string { // if(node.right.type == 'Identifier'){ // this.set_error(`cannot have identifier after ${node.operator}`) // return "" // } return `(${this.traverse_node(node.left)} ${node.operator} ${this.traverse_node(node.right)})`; } // protected override traverse_LogicalExpression(node: jsep.LogicalExpression): string { // // || or && // // if(node.right.type == 'Identifier'){ // // this.set_error(`cannot have identifier after ${node.operator}`) // // return "" // // } // return `(${this.traverse_node(node.left)} ${node.operator} ${this.traverse_node(node.right)})`; // } // protected override traverse_MemberExpression(node: jsep.MemberExpression): string { // return `${this.traverse_node(node.object)}.${this.traverse_node(node.property)}`; // } protected traverse_UnaryExpression(node: jsep.UnaryExpression): string { if (node.operator === ATTRIBUTE_PREFIX) { this._entitiesDependent = true; let argument = node.argument; let attributeName: string | undefined; let property; switch (argument.type) { case 'Identifier': { const argument_identifier = (<unknown>argument) as jsep.Identifier; attributeName = argument_identifier.name; break; } case 'MemberExpression': { const argument_member_expression = (<unknown>argument) as jsep.MemberExpression; const attrib_node = argument_member_expression.object as jsep.Identifier; const property_node = argument_member_expression.property as jsep.Identifier; attributeName = attrib_node.name; property = property_node.name; break; } } // this.function_pre_body += ` // param.entity_attrib_value(${QUOTE}${attrib_node.name}${QUOTE}, param.entity_attrib_values.position); // ` if (attributeName) { attributeName = CoreAttribute.remapName(attributeName); if (attributeName == Attribute.POINT_INDEX || attributeName == Attribute.OBJECT_INDEX) { return `((${VAR_ENTITY} != null) ? ${VAR_ENTITY}.index() : 0)`; } else { const var_attribute_size = this._attribute_requirements_controller.varAttributeSize(attributeName); const var_array = this._attribute_requirements_controller.varArray(attributeName); this._attribute_requirements_controller.add(attributeName); let propertyOffset = property ? PROPERTY_OFFSETS[property] : 0; if (propertyOffset == null) { propertyOffset = 0; } // if (property) { // const property_offset = PROPERTY_OFFSETS[property]; // return `${var_array}[entity.index()*${var_attribute_size}+${property_offset}]`; // } else { // return `${var_array}[entity.index()*${var_attribute_size}]`; // } return `${FUNC_GET_ENTITY_ATTRIBUTE_VALUE}(${VAR_ENTITY}, '${attributeName}', ${var_array}, ${var_attribute_size}, ${propertyOffset})`; } } else { console.warn('attribute not found'); return ''; } } else { return `${node.operator}${this.traverse_node(node.argument)}`; // -5 } } // protected override traverse_Literal(node: jsep.Literal): string { // return `${node.raw}`; // 5 or 'string' (raw will include quotes) // } protected traverse_Identifier(node: jsep.Identifier): string | undefined { const identifier_first_char = node.name[0]; if (identifier_first_char == VARIABLE_PREFIX) { const identifier_name_without_dollar_sign = node.name.substring(1); // globals constants: Math.PI or Math.E const direct_constant_name = GLOBAL_CONSTANTS[identifier_name_without_dollar_sign]; if (direct_constant_name) { return direct_constant_name; } // scene or node globals: $F, $T, $OS, $CH, $OS const method_name = `traverse_Identifier_${identifier_name_without_dollar_sign}`; const method = (this as any)[method_name]; if (method) { return (this as any)[method_name](); } else { this.setError(`identifier unknown: ${node.name}`); } } else { return node.name; // @ptnum will call this method and return "ptnum" } } // // // Identifier methods (called from Identifier_body) // // protected traverse_Identifier_F(): string { this.immutableDependencies.push(this.param.scene().timeController.graphNode); return `param.scene().timeController.frame()`; } // protected traverse_Identifier_FPS(): string { // this.immutable_dependencies.push(this.param.scene().timeController.graphNode); // return `param.scene().timeController.fps`; // } protected traverse_Identifier_T(): string { this.immutableDependencies.push(this.param.scene().timeController.graphNode); return `param.scene().timeController.time()`; } protected traverse_Identifier_OS(): string { const nameNode = this.param.node.nameController.graphNode(); this.param.addGraphInput(nameNode); return `param.node.name()`; // return `${QUOTE}${this.param.node.name()}${QUOTE}`; } protected traverse_Identifier_CH(): string { return `${QUOTE}${this.param.name()}${QUOTE}`; } protected traverse_Identifier_CEX(): string { return this._method_centroid('x'); } protected traverse_Identifier_CEY(): string { return this._method_centroid('y'); } protected traverse_Identifier_CEZ(): string { return this._method_centroid('z'); } // TODO: // '$OS': '_eval_identifier_as_node_name', // '$BBX': '_eval_identifier_as_bounding_box_relative', private _method_centroid(component: string): string { const method_arguments = [0, `${QUOTE}${component}${QUOTE}`]; const arguments_joined = method_arguments.join(ARGUMENTS_SEPARATOR); this._createMethodAndDependencies('centroid', 0); return `(await methods[${this.method_index}].processArguments([${arguments_joined}]))`; } // // // Methods dependencies // // private _createMethodAndDependencies( methodName: string, pathArgument: number | string | undefined, pathNode?: jsep.Expression ) { const expressionRegister = Poly.expressionsRegister; const methodConstructor = expressionRegister.getMethod(methodName); if (!methodConstructor) { const availableMethods = expressionRegister.availableMethods(); const message = `method not found (${methodName}), available methods are: ${availableMethods.join(', ')}`; this.setError(message); Poly.warn(message); return; } const method = new methodConstructor(this.param) as BaseMethod; this.method_index += 1; this.methods[this.method_index] = method; const methodDependency = method.findDependency({indexOrPath: pathArgument}); if (methodDependency) { if (pathNode) { methodDependency.set_jsep_node(pathNode); } this.methodDependencies.push(methodDependency); } else { if (pathNode && isString(pathArgument)) { this.param.scene().missingExpressionReferencesController.register(this.param, pathArgument, pathNode); } } // method_dependencies.resolved_graph_nodes.forEach((graph_node)=>{ // if(path_node){ // const jsep_dependency = new JsepDependency(graph_node, path_node) // this.jsep_dependencies.push(jsep_dependency) // } else { // this.immutable_dependencies.push(graph_node) // } // }) // if(path_node){ // reference_search_result.missing_paths.forEach((path)=>{ // this.jsep_nodes_by_missing_paths[path] = this.jsep_nodes_by_missing_paths[path] || [] // this.jsep_nodes_by_missing_paths[path].push(path_node) // }) // } } }