Skip to content
2 changes: 2 additions & 0 deletions src/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { default as NodeAttribute } from './core/NodeAttribute.js';
export { default as NodeBuilder } from './core/NodeBuilder.js';
export { default as NodeCache } from './core/NodeCache.js';
export { default as NodeCode } from './core/NodeCode.js';
export { default as NodeError } from './core/NodeError.js';
export { default as NodeFrame } from './core/NodeFrame.js';
export { default as NodeFunctionInput } from './core/NodeFunctionInput.js';
export { default as NodeUniform } from './core/NodeUniform.js';
Expand All @@ -28,6 +29,7 @@ export { default as OutputStructNode } from './core/OutputStructNode.js';
export { default as ParameterNode } from './core/ParameterNode.js';
export { default as PropertyNode } from './core/PropertyNode.js';
export { default as StackNode } from './core/StackNode.js';
export { default as StackTrace } from './core/StackTrace.js';
export { default as StructNode } from './core/StructNode.js';
export { default as StructTypeNode } from './core/StructTypeNode.js';
export { default as SubBuildNode } from './core/SubBuildNode.js';
Expand Down
4 changes: 3 additions & 1 deletion src/nodes/accessors/TextureNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Compatibility, IntType, LessCompare, NearestFilter, UnsignedIntType } f
import { Texture } from '../../textures/Texture.js';
import { warn, warnOnce } from '../../utils.js';

import NodeError from '../core/NodeError.js';

const EmptyTexture = /*@__PURE__*/ new Texture();

/**
Expand Down Expand Up @@ -344,7 +346,7 @@ class TextureNode extends UniformNode {

if ( ! texture || texture.isTexture !== true ) {

throw new Error( 'THREE.TSL: `texture( value )` function expects a valid instance of THREE.Texture().' );
throw new NodeError( 'THREE.TSL: `texture( value )` function expects a valid instance of THREE.Texture().', this.stackTrace );

}

Expand Down
24 changes: 24 additions & 0 deletions src/nodes/core/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { EventDispatcher } from '../../core/EventDispatcher.js';
import { MathUtils } from '../../math/MathUtils.js';
import { warn, error } from '../../utils.js';

import StackTrace from './StackTrace.js';

const _parentBuildStage = {
analyze: 'setup',
generate: 'analyze'
Expand Down Expand Up @@ -142,6 +144,20 @@ class Node extends EventDispatcher {

Object.defineProperty( this, 'id', { value: _nodeId ++ } );

/**
* The stack trace of the node for debugging purposes.
*
* @type {?string}
* @default null
*/
this.stackTrace = null;

if ( Node.enableStackTrace === true ) {

this.stackTrace = new StackTrace();

}

}

/**
Expand Down Expand Up @@ -1080,4 +1096,12 @@ class Node extends EventDispatcher {

}

/**
* Enables or disables the automatic capturing of stack traces for nodes.
*
* @type {boolean}
* @default false
*/
Node.enableStackTrace = false;

export default Node;
30 changes: 30 additions & 0 deletions src/nodes/core/NodeError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import StackTrace from './StackTrace.js';

Check failure on line 1 in src/nodes/core/NodeError.js

View workflow job for this annotation

GitHub Actions / Lint, Unit, Unit addons, Circular dependencies & Examples testing

'StackTrace' is defined but never used

/**
* Custom error class for node-related errors, including stack trace information.
*/
class NodeError extends Error {

constructor( message, stackTrace = null ) {

super( message );

/**
* The name of the error.
*
* @type {string}
*/
this.name = 'NodeError';

/**
* The stack trace associated with the error.
*
* @type {?StackTrace}
*/
this.stackTrace = stackTrace;

}

}

export default NodeError;
57 changes: 57 additions & 0 deletions src/nodes/core/StackTrace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Pre-compiled RegExp patterns for ignored files
const IGNORED_FILES = [
/^StackTrace\.js$/,
/^TSLCore\.js$/,
/^.*Node\.js$/,
/^three\.webgpu.*\.js$/
];

/**
* Parses the stack trace and filters out ignored files.
* Returns an array with function name, file, line, and column.
*/
function getFilteredTrace( stack ) {

// Pattern to extract: 1.Function, 2.File, 3.Line, 4.Column
const regex = /(?:at\s+)?([^@\s(]+)?(?:@|\s\()?.*?([^/)]+):(\d+):(\d+)/;

return stack.split( '\n' )
.map( line => {

const match = line.match( regex );
if ( ! match ) return null; // Skip if line format is invalid

return {
fn: match[ 1 ] || 'anonymous', // Function name
file: match[ 2 ].split( '?' )[ 0 ], // Clean file name (Vite/HMR)
line: parseInt( match[ 3 ], 10 ), // Line number
column: parseInt( match[ 4 ], 10 ) // Column number (Added back)
};

} )
.filter( frame => {

// Only keep frames that are valid and not in the ignore list
return frame && ! IGNORED_FILES.some( regex => regex.test( frame.file ) );

} );

}

/**
* Class representing a stack trace for debugging purposes.
*/
class StackTrace {

/**
* Creates a StackTrace instance by capturing and filtering the current stack trace.
*/
constructor() {

this.stack = getFilteredTrace( new Error().stack );

}

}

export default StackTrace;
5 changes: 3 additions & 2 deletions src/nodes/tsl/TSLCore.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SetNode from '../utils/SetNode.js';
import FlipNode from '../utils/FlipNode.js';
import ConstNode from '../core/ConstNode.js';
import MemberNode from '../utils/MemberNode.js';
import StackTrace from '../core/StackTrace.js';
import { getValueFromType, getValueType } from '../core/NodeUtils.js';
import { warn, error } from '../../utils.js';

Expand Down Expand Up @@ -374,13 +375,13 @@ const ShaderNodeProxy = function ( NodeClass, scope = null, factor = null, setti

if ( minParams !== undefined && params.length < minParams ) {

error( `TSL: "${ tslName }" parameter length is less than minimum required.` );
error( `TSL: "${ tslName }" parameter length is less than minimum required.`, new StackTrace() );

return params.concat( new Array( minParams - params.length ).fill( 0 ) );

} else if ( maxParams !== undefined && params.length > maxParams ) {

error( `TSL: "${ tslName }" parameter length exceeds limit.` );
error( `TSL: "${ tslName }" parameter length exceeds limit.`, new StackTrace() );

return params.slice( 0, maxParams );

Expand Down
12 changes: 11 additions & 1 deletion src/renderers/common/nodes/NodeManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,17 @@ class NodeManager extends DataMap {
nodeBuilder = createNodeBuilder( new NodeMaterial() );
nodeBuilder.build();

error( 'TSL: ' + e );
const message = 'TSL: ' + e;

if ( e.stackTrace ) {

error( message, e.stackTrace );

} else {

error( message );

}

}

Expand Down
Loading