MCP 3D Printer Server
by DMontgomery40
Verified
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';
/** @module ScriptableNode **/
/**
* 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();
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.
*
* @function
* @param {CodeNode?} [codeNode=null] - The code node.
* @param {Object} [parameters={}] - The parameters definition.
* @returns {ScriptableNode}
*/
export const scriptable = /*@__PURE__*/ nodeProxy( ScriptableNode );