489 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			489 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import Node, { addNodeClass } from '../core/Node.js';
 | 
						|
import { scriptableValue } from './ScriptableValueNode.js';
 | 
						|
import { addNodeElement, nodeProxy, float } 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 = {};
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	setup() {
 | 
						|
 | 
						|
		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', ScriptableNode );
 |