@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
737 lines (695 loc) • 23.7 kB
text/typescript
/**
* 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)
// })
// }
}
}