UNPKG

@openhps/core

Version:

Open Hybrid Positioning System - Core component

530 lines (490 loc) 13.7 kB
import Node from '../core/Node.js'; import { scriptableValue } from './ScriptableValueNode.js'; import { nodeProxy, float } from '../tsl/TSLBase.js'; import { hashArray, hashString } from '../core/NodeUtils.js'; /** * A Map-like data structure for managing resources of scriptable nodes. * * @augments Map */ class Resources extends Map { get(key, callback = null, ...params) { if (this.has(key)) return super.get(key); if (callback !== null) { const value = callback(...params); this.set(key, value); return value; } } } class Parameters { constructor(scriptableNode) { this.scriptableNode = scriptableNode; } get parameters() { return this.scriptableNode.parameters; } get layout() { return this.scriptableNode.getLayout(); } getInputLayout(id) { return this.scriptableNode.getInputLayout(id); } get(name) { const param = this.parameters[name]; const value = param ? param.getValue() : null; return value; } } /** * Defines the resources (e.g. namespaces) of scriptable nodes. * * @type {Resources} */ export const ScriptableNodeResources = new Resources(); /** * This type of node allows to implement nodes with custom scripts. The script * section is represented as an instance of `CodeNode` written with JavaScript. * The script itself must adhere to a specific structure. * * - main(): Executed once by default and every time `node.needsUpdate` is set. * - layout: The layout object defines the script's interface (inputs and outputs). * * ```js * ScriptableNodeResources.set( 'TSL', TSL ); * * const scriptableNode = scriptable( js( ` * layout = { * outputType: 'node', * elements: [ * { name: 'source', inputType: 'node' }, * ] * }; * * const { mul, oscSine } = TSL; * * function main() { * const source = parameters.get( 'source' ) || float(); * return mul( source, oscSine() ) ); * } * * ` ) ); * * scriptableNode.setParameter( 'source', color( 1, 0, 0 ) ); * * const material = new THREE.MeshBasicNodeMaterial(); * material.colorNode = scriptableNode; * ``` * * @augments Node */ class ScriptableNode extends Node { static get type() { return 'ScriptableNode'; } /** * Constructs a new scriptable node. * * @param {?CodeNode} [codeNode=null] - The code node. * @param {Object} [parameters={}] - The parameters definition. */ constructor(codeNode = null, parameters = {}) { super(); /** * The code node. * * @type {?CodeNode} * @default null */ this.codeNode = codeNode; /** * The parameters definition. * * @type {Object} * @default {} */ this.parameters = parameters; this._local = new Resources(); this._output = scriptableValue(null); this._outputs = {}; this._source = this.source; this._method = null; this._object = null; this._value = null; this._needsOutputUpdate = true; this.onRefresh = this.onRefresh.bind(this); /** * This flag can be used for type testing. * * @type {boolean} * @readonly * @default true */ this.isScriptableNode = true; } /** * The source code of the scriptable node. * * @type {string} */ get source() { return this.codeNode ? this.codeNode.code : ''; } /** * Sets the reference of a local script variable. * * @param {string} name - The variable name. * @param {Object} value - The reference to set. * @return {Resources} The resource map */ setLocal(name, value) { return this._local.set(name, value); } /** * Gets the value of a local script variable. * * @param {string} name - The variable name. * @return {Object} The value. */ getLocal(name) { return this._local.get(name); } /** * Event listener for the `refresh` event. */ onRefresh() { this._refresh(); } /** * Returns an input from the layout with the given id/name. * * @param {string} id - The id/name of the input. * @return {Object} The element entry. */ getInputLayout(id) { for (const element of this.getLayout()) { if (element.inputType && (element.id === id || element.name === id)) { return element; } } } /** * Returns an output from the layout with the given id/name. * * @param {string} id - The id/name of the output. * @return {Object} The element entry. */ getOutputLayout(id) { for (const element of this.getLayout()) { if (element.outputType && (element.id === id || element.name === id)) { return element; } } } /** * Defines a script output for the given name and value. * * @param {string} name - The name of the output. * @param {Node} value - The node value. * @return {ScriptableNode} A reference to this node. */ setOutput(name, value) { const outputs = this._outputs; if (outputs[name] === undefined) { outputs[name] = scriptableValue(value); } else { outputs[name].value = value; } return this; } /** * Returns a script output for the given name. * * @param {string} name - The name of the output. * @return {ScriptableValueNode} The node value. */ getOutput(name) { return this._outputs[name]; } /** * Returns a parameter for the given name * * @param {string} name - The name of the parameter. * @return {ScriptableValueNode} The node value. */ getParameter(name) { return this.parameters[name]; } /** * Sets a value for the given parameter name. * * @param {string} name - The parameter name. * @param {any} value - The parameter value. * @return {ScriptableNode} A reference to this node. */ setParameter(name, value) { const parameters = this.parameters; if (value && value.isScriptableNode) { this.deleteParameter(name); parameters[name] = value; parameters[name].getDefaultOutput().events.addEventListener('refresh', this.onRefresh); } else if (value && value.isScriptableValueNode) { this.deleteParameter(name); parameters[name] = value; parameters[name].events.addEventListener('refresh', this.onRefresh); } else if (parameters[name] === undefined) { parameters[name] = scriptableValue(value); parameters[name].events.addEventListener('refresh', this.onRefresh); } else { parameters[name].value = value; } return this; } /** * Returns the value of this node which is the value of * the default output. * * @return {Node} The value. */ getValue() { return this.getDefaultOutput().getValue(); } /** * Deletes a parameter from the script. * * @param {string} name - The parameter to remove. * @return {ScriptableNode} A reference to this node. */ deleteParameter(name) { let valueNode = this.parameters[name]; if (valueNode) { if (valueNode.isScriptableNode) valueNode = valueNode.getDefaultOutput(); valueNode.events.removeEventListener('refresh', this.onRefresh); } return this; } /** * Deletes all parameters from the script. * * @return {ScriptableNode} A reference to this node. */ clearParameters() { for (const name of Object.keys(this.parameters)) { this.deleteParameter(name); } this.needsUpdate = true; return this; } /** * Calls a function from the script. * * @param {string} name - The function name. * @param {...any} params - A list of parameters. * @return {any} The result of the function call. */ call(name, ...params) { const object = this.getObject(); const method = object[name]; if (typeof method === 'function') { return method(...params); } } /** * Asynchronously calls a function from the script. * * @param {string} name - The function name. * @param {...any} params - A list of parameters. * @return {Promise<any>} The result of the function call. */ async callAsync(name, ...params) { const object = this.getObject(); const method = object[name]; if (typeof method === 'function') { return method.constructor.name === 'AsyncFunction' ? await method(...params) : method(...params); } } /** * Overwritten since the node types is inferred from the script's output. * * @param {NodeBuilder} builder - The current node builder * @return {string} The node type. */ getNodeType(builder) { return this.getDefaultOutputNode().getNodeType(builder); } /** * Refreshes the script node. * * @param {?string} [output=null] - An optional output. */ refresh(output = null) { if (output !== null) { this.getOutput(output).refresh(); } else { this._refresh(); } } /** * Returns an object representation of the script. * * @return {Object} The result object. */ getObject() { if (this.needsUpdate) this.dispose(); if (this._object !== null) return this._object; // const refresh = () => this.refresh(); const setOutput = (id, value) => this.setOutput(id, value); const parameters = new Parameters(this); const THREE = ScriptableNodeResources.get('THREE'); const TSL = ScriptableNodeResources.get('TSL'); const method = this.getMethod(); const params = [parameters, this._local, ScriptableNodeResources, refresh, setOutput, THREE, TSL]; this._object = method(...params); const layout = this._object.layout; if (layout) { if (layout.cache === false) { this._local.clear(); } // default output this._output.outputType = layout.outputType || null; if (Array.isArray(layout.elements)) { for (const element of layout.elements) { const id = element.id || element.name; if (element.inputType) { if (this.getParameter(id) === undefined) this.setParameter(id, null); this.getParameter(id).inputType = element.inputType; } if (element.outputType) { if (this.getOutput(id) === undefined) this.setOutput(id, null); this.getOutput(id).outputType = element.outputType; } } } } return this._object; } deserialize(data) { super.deserialize(data); for (const name in this.parameters) { let valueNode = this.parameters[name]; if (valueNode.isScriptableNode) valueNode = valueNode.getDefaultOutput(); valueNode.events.addEventListener('refresh', this.onRefresh); } } /** * Returns the layout of the script. * * @return {Object} The script's layout. */ getLayout() { return this.getObject().layout; } /** * Returns default node output of the script. * * @return {Node} The default node output. */ getDefaultOutputNode() { const output = this.getDefaultOutput().value; if (output && output.isNode) { return output; } return float(); } /** * Returns default output of the script. * * @return {ScriptableValueNode} The default output. */ getDefaultOutput() { return this._exec()._output; } /** * Returns a function created from the node's script. * * @return {Function} The function representing the node's code. */ getMethod() { if (this.needsUpdate) this.dispose(); if (this._method !== null) return this._method; // const parametersProps = ['parameters', 'local', 'global', 'refresh', 'setOutput', 'THREE', 'TSL']; const interfaceProps = ['layout', 'init', 'main', 'dispose']; const properties = interfaceProps.join(', '); const declarations = 'var ' + properties + '; var output = {};\n'; const returns = '\nreturn { ...output, ' + properties + ' };'; const code = declarations + this.codeNode.code + returns; // this._method = new Function(...parametersProps, code); return this._method; } /** * Frees all internal resources. */ dispose() { if (this._method === null) return; if (this._object && typeof this._object.dispose === 'function') { this._object.dispose(); } this._method = null; this._object = null; this._source = null; this._value = null; this._needsOutputUpdate = true; this._output.value = null; this._outputs = {}; } setup() { return this.getDefaultOutputNode(); } getCacheKey(force) { const values = [hashString(this.source), this.getDefaultOutputNode().getCacheKey(force)]; for (const param in this.parameters) { values.push(this.parameters[param].getCacheKey(force)); } return hashArray(values); } set needsUpdate(value) { if (value === true) this.dispose(); } get needsUpdate() { return this.source !== this._source; } /** * Executes the `main` function of the script. * * @private * @return {ScriptableNode} A reference to this node. */ _exec() { if (this.codeNode === null) return this; if (this._needsOutputUpdate === true) { this._value = this.call('main'); this._needsOutputUpdate = false; } this._output.value = this._value; return this; } /** * Executes the refresh. * * @private */ _refresh() { this.needsUpdate = true; this._exec(); this._output.refresh(); } } export default ScriptableNode; /** * TSL function for creating a scriptable node. * * @tsl * @function * @param {CodeNode} [codeNode] - The code node. * @param {?Object} [parameters={}] - The parameters definition. * @returns {ScriptableNode} */ export const scriptable = /*@__PURE__*/nodeProxy(ScriptableNode).setParameterLength(1, 2);