|
@@ -0,0 +1,488 @@
|
|
|
+import Node, { addNodeClass } from '../core/Node.js';
|
|
|
+import { scriptableValue } from './ScriptableValueNode.js';
|
|
|
+import { addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';
|
|
|
+
|
|
|
+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;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+export const global = new Resources();
|
|
|
+
|
|
|
+class ScriptableNode extends Node {
|
|
|
+
|
|
|
+ constructor( codeNode = null, parameters = {} ) {
|
|
|
+
|
|
|
+ super();
|
|
|
+
|
|
|
+ this.codeNode = codeNode;
|
|
|
+ this.parameters = parameters;
|
|
|
+
|
|
|
+ this._local = new Resources();
|
|
|
+ this._output = scriptableValue();
|
|
|
+ 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.isScriptableNode = true;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ get source() {
|
|
|
+
|
|
|
+ return this.codeNode ? this.codeNode.code : '';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ setLocal( name, value ) {
|
|
|
+
|
|
|
+ return this._local.set( name, value );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getLocal( name ) {
|
|
|
+
|
|
|
+ return this._local.get( name );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ onRefresh() {
|
|
|
+
|
|
|
+ this._refresh();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getInputLayout( id ) {
|
|
|
+
|
|
|
+ for ( const element of this.getLayout() ) {
|
|
|
+
|
|
|
+ if ( element.inputType && ( element.id === id || element.name === id ) ) {
|
|
|
+
|
|
|
+ return element;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getOutputLayout( id ) {
|
|
|
+
|
|
|
+ for ( const element of this.getLayout() ) {
|
|
|
+
|
|
|
+ if ( element.outputType && ( element.id === id || element.name === id ) ) {
|
|
|
+
|
|
|
+ return element;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ setOutput( name, value ) {
|
|
|
+
|
|
|
+ const outputs = this._outputs;
|
|
|
+
|
|
|
+ if ( outputs[ name ] === undefined ) {
|
|
|
+
|
|
|
+ outputs[ name ] = scriptableValue( value );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ outputs[ name ].value = value;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return this;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getOutput( name ) {
|
|
|
+
|
|
|
+ return this._outputs[ name ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getParameter( name ) {
|
|
|
+
|
|
|
+ return this.parameters[ name ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getValue() {
|
|
|
+
|
|
|
+ return this.getDefaultOutput().getValue();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ deleteParameter( name ) {
|
|
|
+
|
|
|
+ let valueNode = this.parameters[ name ];
|
|
|
+
|
|
|
+ if ( valueNode ) {
|
|
|
+
|
|
|
+ if ( valueNode.isScriptableNode ) valueNode = valueNode.getDefaultOutput();
|
|
|
+
|
|
|
+ valueNode.events.removeEventListener( 'refresh', this.onRefresh );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return this;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ clearParameters() {
|
|
|
+
|
|
|
+ for ( const name of Object.keys( this.parameters ) ) {
|
|
|
+
|
|
|
+ this.deleteParameter( name );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ this.needsUpdate = true;
|
|
|
+
|
|
|
+ return this;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ call( name, ...params ) {
|
|
|
+
|
|
|
+ const object = this.getObject();
|
|
|
+ const method = object[ name ];
|
|
|
+
|
|
|
+ if ( typeof method === 'function' ) {
|
|
|
+
|
|
|
+ return method( ...params );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getNodeType( builder ) {
|
|
|
+
|
|
|
+ return this.getDefaultOutputNode().getNodeType( builder );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ refresh( output = null ) {
|
|
|
+
|
|
|
+ if ( output !== null ) {
|
|
|
+
|
|
|
+ this.getOutput( output ).refresh();
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ this._refresh();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 = global.get( 'THREE' );
|
|
|
+ const TSL = global.get( 'TSL' );
|
|
|
+
|
|
|
+ const method = this.getMethod( this.codeNode );
|
|
|
+ const params = [ parameters, this._local, global, 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 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getLayout() {
|
|
|
+
|
|
|
+ return this.getObject().layout;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getDefaultOutputNode() {
|
|
|
+
|
|
|
+ const output = this.getDefaultOutput().value;
|
|
|
+
|
|
|
+ if ( output && output.isNode ) {
|
|
|
+
|
|
|
+ return output;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return float();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ getDefaultOutput() {
|
|
|
+
|
|
|
+ return this._exec()._output;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 = {};
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ construct() {
|
|
|
+
|
|
|
+ return this.getDefaultOutputNode();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ set needsUpdate( value ) {
|
|
|
+
|
|
|
+ if ( value === true ) this.dispose();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ get needsUpdate() {
|
|
|
+
|
|
|
+ return this.source !== this._source;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ _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;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ _refresh() {
|
|
|
+
|
|
|
+ this.needsUpdate = true;
|
|
|
+
|
|
|
+ this._exec();
|
|
|
+
|
|
|
+ this._output.refresh();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+export default ScriptableNode;
|
|
|
+
|
|
|
+export const scriptable = nodeProxy( ScriptableNode );
|
|
|
+
|
|
|
+addNodeElement( 'scriptable', scriptable );
|
|
|
+
|
|
|
+addNodeClass( ScriptableNode );
|