UNPKG

three

Version:

JavaScript 3D library

1,521 lines (1,038 loc) • 36.5 kB
import { NodeUpdateType } from './constants.js'; import { hash, hashArray, hashString } from './NodeUtils.js'; import { EventDispatcher } from '../../core/EventDispatcher.js'; import { generateUUID } from '../../math/MathUtils.js'; import { warn, error } from '../../utils.js'; import StackTrace from './StackTrace.js'; const _parentBuildStage = { analyze: 'setup', generate: 'analyze' }; let _nodeId = 0; /** * Base class for all nodes. * * @augments EventDispatcher */ class Node extends EventDispatcher { static get type() { return 'Node'; } /** * Constructs a new node. * * @param {?string} nodeType - The node type. */ constructor( nodeType = null ) { super(); /** * The node type. This represents the result type of the node (e.g. `float` or `vec3`). * * @type {?string} * @default null */ this.nodeType = nodeType; /** * The update type of the node's {@link Node#update} method. Possible values are listed in {@link NodeUpdateType}. * * @type {string} * @default 'none' */ this.updateType = NodeUpdateType.NONE; /** * The update type of the node's {@link Node#updateBefore} method. Possible values are listed in {@link NodeUpdateType}. * * @type {string} * @default 'none' */ this.updateBeforeType = NodeUpdateType.NONE; /** * The update type of the node's {@link Node#updateAfter} method. Possible values are listed in {@link NodeUpdateType}. * * @type {string} * @default 'none' */ this.updateAfterType = NodeUpdateType.NONE; /** * The version of the node. The version automatically is increased when {@link Node#needsUpdate} is set to `true`. * * @type {number} * @readonly * @default 0 */ this.version = 0; /** * The name of the node. * * @type {string} * @default '' */ this.name = ''; /** * Whether this node is global or not. This property is relevant for the internal * node caching system. All nodes which should be declared just once should * set this flag to `true` (a typical example is {@link AttributeNode}). * * @type {boolean} * @default false */ this.global = false; /** * Create a list of parents for this node during the build process. * * @type {boolean} * @default false */ this.parents = false; /** * Whether this node can be assigned. It is generated as an expression, * unless it is assigned, in which case it is declared as a variable where it was created. * * @type {boolean} * @default false */ this.assignable = false; /** * This flag can be used for type testing. * * @type {boolean} * @readonly * @default true */ this.isNode = true; // private this._inspectorNode = null; /** * The cache key of this node. * * @private * @type {?number} * @default null */ this._cacheKey = null; /** * The UUID of the node. * * @type {string} * @default null * @private */ this._uuid = null; /** * The cache key's version. * * @private * @type {number} * @default 0 */ this._cacheKeyVersion = 0; /** * The unique ID of the node. * * @type {number} * @readonly */ this.id = _nodeId ++; /** * The stack trace of the node for debugging purposes. * * @type {?string} * @default null */ this.stackTrace = null; if ( Node.captureStackTrace === true ) { this.stackTrace = new StackTrace(); } } /** * Whether this node allows caching its result in a temporary variable. * Caching is enabled by default. Override this method to disable it. * * @param {NodeBuilder} builder - The current node builder. * @return {boolean} Whether temporary caching is allowed. */ isCacheable( /*builder*/ ) { return true; } /** * Makes this node assignable and adds it to the current stack, * so it can be declared where it was created if it is assigned later. * Only use it with nodes that are not shared, see {@link Node#assignable}. * * @return {Node} A reference to this node. */ toAssignable() { this.assignable = true; return this.toStack(); } /** * Checks if this node is assignable without a forced declaration. * * @param {NodeBuilder} builder - The node builder. * @returns {boolean} Whether this node is assignable without a forced declaration. */ isAssignable( builder ) { const data = builder.getDataFromNode( this ); if ( data.forceDeclaration === true ) return false; return this.assignable; } /** * Checks if this node is the target of an assignment. * * @param {NodeBuilder} builder - The node builder. * @returns {boolean} Whether this node is assigned. */ isAssign( builder ) { const data = builder.getDataFromNode( this ); return data.assign; } /** * Declares the assignable node before the loops where it is used if it has no stack, and * decides if a value that cannot be cached must be evaluated once at its declaration. * * @private * @param {NodeBuilder} builder - The current node builder. * @param {Object} data - The node data. * @param {Node} [valueNode=this] - The node that defines the value of the assignable node. */ _updateAssignable( builder, data, valueNode = this ) { const nodeLoop = builder.context.nodeLoop; if ( data.stack === undefined && builder.buildStage === 'setup' ) { // A node outside of the stacks of this build, e.g. created outside of a function, is declared // before the outermost loop where its value does not change, otherwise every iteration would repeat it. data.stack = builder.declareBeforeBlock( this ); } else if ( this.assignable === true && nodeLoop && builder.buildStage === 'analyze' ) { if ( valueNode.isCacheable( builder ) === false && builder.isDeterministic( valueNode ) === false ) { // A value that cannot be cached, e.g. a function call, is evaluated once at its declaration // if it is used in a loop that runs after it, otherwise the loop would repeat it. const declarationIndex = builder.activeStacks.indexOf( data.stack ); const loopIndex = builder.activeStacks.indexOf( builder.getDataFromNode( nodeLoop ).stack ); if ( declarationIndex !== - 1 && loopIndex >= declarationIndex ) data.forceDeclaration = true; } } } /** * Whether `build()` declares this assignable node as a variable when it is assigned. * Nodes that wrap an assignable value, like {@link VarNode}, declare it themselves. * * @private * @return {boolean} Whether `build()` declares the assignable node. */ _declaresAssignable() { return true; } /** * Declares a variable for this node in the current flow, initialized with the given snippet. * * @private * @param {NodeBuilder} builder - The current node builder. * @param {string} snippet - The initial value. * @param {string} type - The variable type. * @param {boolean} readOnly - Whether the variable is read-only. * @param {string} property - The node data property that holds the variable. * @return {string} The property name of the variable. */ _declareVariable( builder, snippet, type, readOnly, property ) { const nodeVar = builder.getVarFromNode( this, null, type, undefined, readOnly, true, property ); const propertyName = builder.getPropertyName( nodeVar ); const declarationPrefix = readOnly ? builder.generateLetStatement( nodeVar.type, propertyName, nodeVar.count ) : builder.generateVarStatement( nodeVar.type, propertyName, nodeVar.count ); builder.addLineFlowCode( `${ declarationPrefix } = ${ snippet }`, this ); return propertyName; } /** * Whether this assignable node must be declared as a variable, because it is assigned * or must be evaluated once at its declaration. * * @private * @param {NodeBuilder} builder - The current node builder. * @param {Object} nodeData - The node data. * @return {boolean} Whether the assignable node is a variable. */ _isVariable( builder, nodeData ) { return ( nodeData.assign === true || nodeData.forceDeclaration === true ) && this.getNodeType( builder ) !== 'void'; } /** * Generates an assignable node as a variable. The variable is declared the first * time it is generated, which is its stack position, and then referenced by name. * * @private * @param {NodeBuilder} builder - The current node builder. * @param {Object} nodeData - The node data. * @param {?string} output - The output type. * @return {string} The generated shader string. */ _buildVariable( builder, nodeData, output ) { const nodeType = this.getNodeType( builder ); const type = builder.getVectorType( nodeType ); if ( nodeData.propertyName === undefined ) { const snippet = this.generate.length < 2 ? builder.format( this.generate( builder ) || '', nodeType, type ) : this.generate( builder, type ) || ''; nodeData.propertyName = this._declareVariable( builder, snippet, type, false, 'variable' ); } return builder.format( nodeData.propertyName, type, output ); } /** * Whether the generated value is a variable declared in the scope of the function, * so it keeps its value after the block where it was generated, e.g. a {@link VarNode}. * * @private * @return {boolean} Whether the generated value is a variable of the function scope. */ _isFunctionScope() { return false; } /** * A generated value is only visible in the block where it was declared and in its inner blocks. * A variable of the function scope is also visible in a later block of the same parent and guard, * e.g. a second `if ( c )` of an immutable condition `c`, since that block only runs if its block ran. * This method discards the generated value if it is not visible in the current block. * * @private * @param {NodeBuilder} builder - The current node builder. * @param {Object} nodeData - The node data. */ _checkFlowBlock( builder, nodeData ) { if ( nodeData.flowBlock !== undefined ) { const { guard, parent } = nodeData.flowBlock; const isFunctionScope = guard !== undefined && this._isFunctionScope(); let flowBlock = builder.flowBlock; while ( flowBlock !== null && flowBlock !== nodeData.flowBlock ) { if ( isFunctionScope && flowBlock.guard === guard && flowBlock.parent === parent ) break; flowBlock = flowBlock.parent; } if ( flowBlock === null ) { nodeData.flowBlock = undefined; nodeData.propertyName = undefined; nodeData.snippet = undefined; nodeData.generated = undefined; } } } /** * Keeps the block where a value was generated, so it is only reused where it is visible. * A global node is a declaration visible in any block, unless it emitted code in this block. * * @private * @param {NodeBuilder} builder - The current node builder. * @param {Object} nodeData - The node data. * @param {boolean} isCached - Whether the value was generated before. * @param {number} flowCodeLength - The length of the flow code before the value was generated. */ _setFlowBlock( builder, nodeData, isCached, flowCodeLength ) { const isLocal = this.isGlobal( builder ) === false || builder.flow.code.length !== flowCodeLength; if ( isCached === false && ( nodeData.propertyName !== undefined || nodeData.snippet !== undefined ) && isLocal && builder.flowBlock !== null ) { nodeData.flowBlock = builder.flowBlock; } } /** * Set this property to `true` when the node should be regenerated. * * @type {boolean} * @default false * @param {boolean} value */ set needsUpdate( value ) { if ( value === true ) { this.version ++; } } /** * The UUID of the node. * * @type {string} * @readonly */ get uuid() { if ( this._uuid === null ) { this._uuid = generateUUID(); } return this._uuid; } /** * The type of the class. The value is usually the constructor name. * * @type {string} * @readonly */ get type() { return this.constructor.type; } /** * Convenient method for defining {@link Node#update}. * * @param {Function} callback - The update method. * @param {string} updateType - The update type. * @return {Node} A reference to this node. */ onUpdate( callback, updateType ) { this.updateType = updateType; this.update = callback.bind( this ); return this; } /** * Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but * this method automatically sets the update type to `FRAME`. * * @param {Function} callback - The update method. * @return {Node} A reference to this node. */ onFrameUpdate( callback ) { return this.onUpdate( callback, NodeUpdateType.FRAME ); } /** * Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but * this method automatically sets the update type to `RENDER`. * * @param {Function} callback - The update method. * @return {Node} A reference to this node. */ onRenderUpdate( callback ) { return this.onUpdate( callback, NodeUpdateType.RENDER ); } /** * Convenient method for defining {@link Node#update}. Similar to {@link Node#onUpdate}, but * this method automatically sets the update type to `OBJECT`. * * @param {Function} callback - The update method. * @return {Node} A reference to this node. */ onObjectUpdate( callback ) { return this.onUpdate( callback, NodeUpdateType.OBJECT ); } /** * Convenient method for defining {@link Node#updateReference}. * * @param {Function} callback - The update method. * @return {Node} A reference to this node. */ onReference( callback ) { this.updateReference = callback.bind( this ); return this; } /** * Nodes might refer to other objects like materials. This method allows to dynamically update the reference * to such objects based on a given state (e.g. the current node frame or builder). * * @param {any} state - This method can be invocated in different contexts so `state` can refer to any object type. * @return {any} The updated reference. */ updateReference( /*state*/ ) { return this; } /** * By default this method returns the value of the {@link Node#global} flag. This method * can be overwritten in derived classes if an analytical way is required to determine the * global cache referring to the current shader-stage. * * @param {NodeBuilder} builder - The current node builder. * @return {boolean} Whether this node is global or not. */ isGlobal( /*builder*/ ) { return this.global; } /** * Returns the child nodes of this node. * * @return {Array<Node>} The child nodes. */ getChildren() { const children = this._getChildren(); const nodes = []; for ( let i = 0; i < children.length; i ++ ) { nodes.push( children[ i ].childNode ); } return nodes; } /** * Calling this method dispatches the `dispose` event. This event can be used * to register event listeners for clean up tasks. */ dispose() { this.dispatchEvent( { type: 'dispose' } ); } /** * Callback for {@link Node#traverse}. * * @callback traverseCallback * @param {Node} node - The current node. */ /** * Can be used to traverse through the node's hierarchy. * * @param {traverseCallback} callback - A callback that is executed per node. */ traverse( callback ) { callback( this ); for ( const childNode of this.getChildren() ) { childNode.traverse( callback ); } } /** * Returns the child nodes of this node. * * @private * @param {Set<Node>} [ignores=new Set()] - A set of nodes to ignore during the search to avoid circular references. * @returns {Array<Object>} An array of objects describing the child nodes. */ _getChildren( ignores = new Set() ) { const children = []; // avoid circular references ignores.add( this ); const properties = Object.getOwnPropertyNames( this ); for ( let p = 0; p < properties.length; p ++ ) { const property = properties[ p ]; // Ignore private properties. if ( property.charCodeAt( 0 ) === 95 ) continue; const object = this[ property ]; // Ignore primitives and ignored nodes. if ( object === null || ( typeof object !== 'object' && typeof object !== 'function' ) || ignores.has( object ) ) continue; if ( Array.isArray( object ) === true ) { for ( let i = 0; i < object.length; i ++ ) { const child = object[ i ]; if ( child && child.isNode === true ) { children.push( { property, index: i, childNode: child } ); } } } else if ( object && object.isNode === true ) { children.push( { property, childNode: object } ); } else if ( object && Object.getPrototypeOf( object ) === Object.prototype ) { for ( const subProperty in object ) { // Ignore private sub-properties. if ( subProperty.startsWith( '_' ) === true ) continue; const child = object[ subProperty ]; if ( child && child.isNode === true ) { children.push( { property, index: subProperty, childNode: child } ); } } } } // return children; } /** * Returns the cache key for this node. * * @param {boolean} [force=false] - When set to `true`, a recomputation of the cache key is forced. * @param {Set<Node>} [ignores=null] - A set of nodes to ignore during the computation of the cache key. * @return {number} The cache key of the node. */ getCacheKey( force = false, ignores = null ) { force = force || this.version !== this._cacheKeyVersion; if ( force === true || this._cacheKey === null ) { if ( ignores === null ) ignores = new Set(); // const values = []; for ( const { property, childNode } of this._getChildren( ignores ) ) { values.push( hashString( property.slice( 0, - 4 ) ), childNode.getCacheKey( force, ignores ) ); } // this._cacheKey = hash( hashArray( values ), this.customCacheKey() ); this._cacheKeyVersion = this.version; } return this._cacheKey; } /** * Generate a custom cache key for this node. * * @return {number} The cache key of the node. */ customCacheKey() { return this.id; } /** * Returns the references to this node which is by default `this`. * * @return {Node} A reference to this node. */ getScope() { return this; } /** * Returns the hash of the node which is used to identify the node. By default it's * the {@link Node#uuid} however derived node classes might have to overwrite this method * depending on their implementation. * * @param {NodeBuilder} builder - The current node builder. * @return {string} The hash. */ getHash( /*builder*/ ) { return String( this.id ); } /** * Returns the update type of {@link Node#update}. * * @param {NodeFrame} [frame] - The current node frame. * @return {NodeUpdateType} The update type. */ getUpdateType( /*frame*/ ) { return this.updateType; } /** * Returns the update type of {@link Node#updateBefore}. * * @param {NodeFrame} [frame] - The current node frame. * @return {NodeUpdateType} The update type. */ getUpdateBeforeType( /*frame*/ ) { return this.updateBeforeType; } /** * Returns the update type of {@link Node#updateAfter}. * * @param {NodeFrame} [frame] - The current node frame. * @return {NodeUpdateType} The update type. */ getUpdateAfterType( /*frame*/ ) { return this.updateAfterType; } /** * Certain types are composed of multiple elements. For example a `vec3` * is composed of three `float` values. This method returns the type of * these elements. * * @param {NodeBuilder} builder - The current node builder. * @return {string} The type of the node. */ getElementType( builder ) { const type = this.getNodeType( builder ); const elementType = builder.getElementType( type ); return elementType; } /** * Returns the node member type for the given name. * * @param {NodeBuilder} builder - The current node builder. * @param {string} name - The name of the member. * @return {string} The type of the node. */ getMemberType( /*builder, name*/ ) { return 'void'; } /** * Returns the node's type. * * @param {NodeBuilder} builder - The current node builder. * @param {string} [output=null] - The output of the node. * @return {string} The type of the node. */ getNodeType( builder, output = null ) { const nodeData = builder.getDataFromNode( this ); let type; if ( output !== null ) { nodeData.typeFromOutput = nodeData.typeFromOutput || {}; type = nodeData.typeFromOutput[ output ]; if ( type === undefined ) { type = this.generateNodeType( builder, output ); nodeData.typeFromOutput[ output ] = type; } } else { type = nodeData.type; if ( type === undefined ) { type = this.generateNodeType( builder ); nodeData.type = type; } } return type; } /** * Returns the node's type. * * @param {NodeBuilder} builder - The current node builder. * @param {string} [output=null] - The output of the node. * @return {string} The type of the node. */ generateNodeType( builder, output = null ) { const nodeProperties = builder.getNodeProperties( this ); if ( nodeProperties.outputNode ) { return nodeProperties.outputNode.getNodeType( builder, output ); } return this.nodeType; } /** * This method is used during the build process of a node and ensures * equal nodes are not built multiple times but just once. For example if * `attribute( 'uv' )` is used multiple times by the user, the build * process makes sure to process just the first node. It also handles * node overrides if an override context is set. * * @param {NodeBuilder} builder - The current node builder. * @return {Node} The shared node if possible. Otherwise `this` is returned. */ getShared( builder ) { const hash = this.getHash( builder ); const nodeFromHash = builder.getNodeFromHash( hash ); let sharedNode = null; if ( nodeFromHash && nodeFromHash !== this ) { sharedNode = nodeFromHash; } else if ( builder.context.overrideNodes ) { const callback = builder.context.overrideNodes.get( this ); if ( callback ) { // the overrides are cached per callback, nested contexts can override the same node with different nodes const nodeData = builder.getDataFromNode( this ); const overrides = nodeData.overrides || ( nodeData.overrides = new Map() ); if ( overrides.has( callback ) === false ) { // avoid recursive override calls overrides.set( callback, null ); // cancel the override for use the same node inside the callback sharedNode = callback( builder ).overrideNode( this, null ); overrides.set( callback, sharedNode ); } else { sharedNode = overrides.get( callback ); } } } return sharedNode || this; } /** * Returns the number of elements in the node array. * * @param {NodeBuilder} builder - The current node builder. * @return {?number} The number of elements in the node array. */ getArrayCount( /*builder*/ ) { return null; } /** * Represents the setup stage which is the first step of the build process, see {@link Node#build} method. * This method is often overwritten in derived modules to prepare the node which is used as a node's output/result. * If an output node is prepared, then it must be returned in the `return` statement of the derived module's setup function. * * @param {NodeBuilder} builder - The current node builder. * @return {?Node} The output node. */ setup( builder ) { const nodeProperties = builder.getNodeProperties( this ); let index = 0; for ( const childNode of this.getChildren() ) { nodeProperties[ 'node' + index ++ ] = childNode; } // return a outputNode if exists or null return nodeProperties.outputNode || null; } /** * Represents the analyze stage which is the second step of the build process, see {@link Node#build} method. * This stage analyzes the node hierarchy and ensures descendent nodes are built. * * @param {NodeBuilder} builder - The current node builder. * @param {?Node} output - The target output node. */ analyze( builder, output = null ) { const usageCount = builder.increaseUsage( this ); if ( this.parents === true ) { const nodeData = builder.getDataFromNode( this, 'any' ); nodeData.stages = nodeData.stages || {}; nodeData.stages[ builder.shaderStage ] = nodeData.stages[ builder.shaderStage ] || []; nodeData.stages[ builder.shaderStage ].push( output ); } if ( usageCount === 1 ) { // node flow children const nodeProperties = builder.getNodeProperties( this ); for ( const childNode of Object.values( nodeProperties ) ) { if ( childNode && childNode.isNode === true ) { childNode.build( builder, this ); } } } } /** * Represents the generate stage which is the third step of the build process, see {@link Node#build} method. * This state builds the output node and returns the resulting shader string. * * @param {NodeBuilder} builder - The current node builder. * @param {?string} [output] - Can be used to define the output type. * @return {?string} The generated shader string. */ generate( builder, output ) { const { outputNode } = builder.getNodeProperties( this ); if ( outputNode && outputNode.isNode === true ) { return outputNode.build( builder, output ); } } /** * The method can be implemented to update the node's internal state before it is used to render an object. * The {@link Node#updateBeforeType} property defines how often the update is executed. * * @abstract * @param {NodeFrame} frame - A reference to the current node frame. * @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching). */ updateBefore( /*frame*/ ) { warn( 'Abstract function.' ); } /** * The method can be implemented to update the node's internal state after it was used to render an object. * The {@link Node#updateAfterType} property defines how often the update is executed. * * @abstract * @param {NodeFrame} frame - A reference to the current node frame. * @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching). */ updateAfter( /*frame*/ ) { warn( 'Abstract function.' ); } /** * The method can be implemented to update the node's internal state when it is used to render an object. * The {@link Node#updateType} property defines how often the update is executed. * * @abstract * @param {NodeFrame} frame - A reference to the current node frame. * @return {?boolean} An optional bool that indicates whether the implementation actually performed an update or not (e.g. due to caching). */ update( /*frame*/ ) { warn( 'Abstract function.' ); } /** * Registers the inspector node added with `toInspector()` in the builder, so it is updated every frame. * Nodes that override `build()` without always calling `super.build()` must call this method. * * @private * @param {NodeBuilder} builder - The current node builder. */ _setupInspector( builder ) { if ( this._inspectorNode !== null && builder.buildStage === 'setup' ) { builder.addNode( this._inspectorNode ); } } /** * This method performs the build of a node. The behavior and return value depend on the current build stage: * - **setup**: Prepares the node and its children for the build process. This process can also create new nodes. Returns the node itself or a variant. * - **analyze**: Analyzes the node hierarchy for optimizations in the code generation stage. Returns `null`. * - **generate**: Generates the shader code for the node. Returns the generated shader string. * * @param {NodeBuilder} builder - The current node builder. * @param {?(string|Node)} [output=null] - Can be used to define the output type. * @return {?(Node|string)} The result of the build process, depending on the build stage. */ build( builder, output = null ) { this._setupInspector( builder ); const refNode = this.getShared( builder ); if ( this !== refNode ) { return refNode.build( builder, output ); } // const nodeData = builder.getDataFromNode( this ); if ( this.assignable === true && this._declaresAssignable() ) this._updateAssignable( builder, nodeData ); nodeData.buildStages = nodeData.buildStages || { setup: false, analyze: false, generate: false }; nodeData.buildStages[ builder.buildStage ] = true; const parentBuildStage = _parentBuildStage[ builder.buildStage ]; if ( parentBuildStage && nodeData.buildStages[ parentBuildStage ] !== true ) { // force parent build stage (setup or analyze) const previousBuildStage = builder.getBuildStage(); builder.setBuildStage( parentBuildStage ); this.build( builder ); builder.setBuildStage( previousBuildStage ); } // builder.addChain( this ); /* Build stages expected results: - "setup" -> Node - "analyze" -> null - "generate" -> String */ let result = null; const buildStage = builder.getBuildStage(); if ( buildStage === 'setup' ) { builder.addNode( this ); this.updateReference( builder ); const properties = builder.getNodeProperties( this ); if ( properties.initialized !== true ) { //const stackNodesBeforeSetup = builder.stack.nodes.length; properties.initialized = true; properties.outputNode = this.setup( builder ) || properties.outputNode || null; /*if ( isNodeOutput && builder.stack.nodes.length !== stackNodesBeforeSetup ) { // !! no outputNode !! //outputNode = builder.stack; }*/ for ( const childNode of Object.values( properties ) ) { if ( childNode && childNode.isNode === true ) { if ( childNode.parents === true ) { const childProperties = builder.getNodeProperties( childNode ); childProperties.parents = childProperties.parents || []; childProperties.parents.push( this ); } childNode.build( builder ); } } builder.addSequentialNode( this ); } result = properties.outputNode; } else if ( buildStage === 'analyze' ) { this.analyze( builder, output ); } else if ( buildStage === 'generate' ) { this._checkFlowBlock( builder, nodeData ); const isCached = nodeData.propertyName !== undefined || nodeData.snippet !== undefined; const flowCodeLength = builder.flow.code.length; // References must be generated directly, even if a cached value exists. const allowedCache = this.isCacheable( builder ) && builder.isReference( output ) === false; const type = allowedCache ? builder.getVectorType( this.getNodeType( builder, output ) ) : null; const cacheResult = allowedCache && type !== 'void' && output !== 'void' && nodeData.usageCount > 1; const generateOutput = cacheResult ? type : output; if ( this.assignable === true && this._declaresAssignable() && this._isVariable( builder, nodeData ) ) { result = this._buildVariable( builder, nodeData, output ); } else if ( allowedCache && nodeData.propertyName !== undefined ) { result = builder.format( nodeData.propertyName, type, output ); } else { // If generate has just one argument, it means the output type is not required. // This means that the node does not handle output conversions internally, // so the value is stored in a cache and the builder handles the conversion // for all requested output types. const isGenerateOnce = this.generate.length < 2; if ( isGenerateOnce ) { const type = this.getNodeType( builder ); result = nodeData.snippet; if ( result === undefined ) { if ( nodeData.generated === undefined ) { nodeData.generated = true; result = this.generate( builder ) || ''; nodeData.snippet = result; } else { warn( 'Node: Recursion detected.', this ); result = '/* Recursion detected. */'; } } result = builder.format( result, type, generateOutput ); } else { result = this.generate( builder, generateOutput ) || ''; } if ( result === '' && generateOutput !== null && generateOutput !== 'void' && generateOutput !== 'OutputType' ) { // if no snippet is generated, return a default value error( `TSL: Invalid generated code, expected a "${ generateOutput }".` ); result = builder.generateConst( generateOutput ); } if ( cacheResult ) { // Use a dedicated property, the node may already own a variable. const propertyName = this._declareVariable( builder, result, type, nodeData.assign !== true, 'cacheVariable' ); nodeData.snippet = result; nodeData.propertyName = propertyName; result = builder.format( propertyName, type, output ); } } this._setFlowBlock( builder, nodeData, isCached, flowCodeLength ); } builder.removeChain( this ); return result; } /** * Returns the child nodes as a JSON object. * * @return {Generator<Object>} An iterable list of serialized child objects as JSON. */ getSerializeChildren() { return this._getChildren(); } /** * Serializes the node to JSON. * * @param {Object} json - The output JSON object. */ serialize( json ) { const nodeChildren = this.getSerializeChildren(); const inputNodes = {}; for ( const { property, index, childNode } of nodeChildren ) { if ( index !== undefined ) { if ( inputNodes[ property ] === undefined ) { inputNodes[ property ] = Number.isInteger( index ) ? [] : {}; } inputNodes[ property ][ index ] = childNode.toJSON( json.meta ).uuid; } else { inputNodes[ property ] = childNode.toJSON( json.meta ).uuid; } } if ( Object.keys( inputNodes ).length > 0 ) { json.inputNodes = inputNodes; } } /** * Deserializes the node from the given JSON. * * @param {Object} json - The JSON object. */ deserialize( json ) { if ( json.inputNodes !== undefined ) { const nodes = json.meta.nodes; for ( const property in json.inputNodes ) { if ( Array.isArray( json.inputNodes[ property ] ) ) { const inputArray = []; for ( const uuid of json.inputNodes[ property ] ) { inputArray.push( nodes[ uuid ] ); } this[ property ] = inputArray; } else if ( typeof json.inputNodes[ property ] === 'object' ) { const inputObject = {}; for ( const subProperty in json.inputNodes[ property ] ) { const uuid = json.inputNodes[ property ][ subProperty ]; inputObject[ subProperty ] = nodes[ uuid ]; } this[ property ] = inputObject; } else { const uuid = json.inputNodes[ property ]; this[ property ] = nodes[ uuid ]; } } } } /** * Serializes the node into the three.js JSON Object/Scene format. * * @param {?Object} meta - An optional JSON object that already holds serialized data from other scene objects. * @return {Object} The serialized node. */ toJSON( meta ) { const { uuid, type } = this; const isRoot = ( meta === undefined || typeof meta === 'string' ); if ( isRoot ) { meta = { textures: {}, images: {}, nodes: {} }; } // serialize let data = meta.nodes[ uuid ]; if ( data === undefined ) { data = { uuid, type, meta, metadata: { version: 4.7, type: 'Node', generator: 'Node.toJSON' } }; if ( isRoot !== true ) meta.nodes[ data.uuid ] = data; this.serialize( data ); delete data.meta; } // TODO: Copied from Object3D.toJSON function extractFromCache( cache ) { const values = []; for ( const key in cache ) { const data = cache[ key ]; delete data.metadata; values.push( data ); } return values; } if ( isRoot ) { const textures = extractFromCache( meta.textures ); const images = extractFromCache( meta.images ); const nodes = extractFromCache( meta.nodes ); if ( textures.length > 0 ) data.textures = textures; if ( images.length > 0 ) data.images = images; if ( nodes.length > 0 ) data.nodes = nodes; } return data; } } /** * Enables or disables the automatic capturing of stack traces for nodes. * * @type {boolean} * @default false */ Node.captureStackTrace = false; export default Node;