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 );
 |