Просмотр исходного кода

Inspector: Added `Extension` support and revisions (#33200)

sunag 4 недель назад
Родитель
Сommit
77e9817133

+ 13 - 0
examples/jsm/inspector/Extension.js

@@ -0,0 +1,13 @@
+import { Tab } from 'three/addons/inspector/ui/Tab.js';
+
+export class Extension extends Tab {
+
+	constructor( name, options = {} ) {
+
+		super( name, options );
+
+		this.isExtension = true;
+
+	}
+
+}

+ 108 - 108
examples/jsm/inspector/Inspector.js

@@ -8,32 +8,9 @@ import { Parameters } from './tabs/Parameters.js';
 import { Settings } from './tabs/Settings.js';
 import { Viewer } from './tabs/Viewer.js';
 import { Timeline } from './tabs/Timeline.js';
-import { setText, splitPath, splitCamelCase } from './ui/utils.js';
+import { setText } from './ui/utils.js';
 
-import { QuadMesh, NodeMaterial, CanvasTarget, setConsoleFunction, REVISION, NoToneMapping } from 'three/webgpu';
-import { renderOutput, vec2, vec3, vec4, Fn, screenUV, step, OnMaterialUpdate, uniform } from 'three/tsl';
-
-const aspectRatioUV = /*@__PURE__*/ Fn( ( [ uv, textureNode ] ) => {
-
-	const aspect = uniform( 0 );
-
-	OnMaterialUpdate( () => {
-
-		const { width, height } = textureNode.value;
-
-		aspect.value = width / height;
-
-	} );
-
-	const centered = uv.sub( 0.5 );
-	const corrected = vec2( centered.x.div( aspect ), centered.y );
-	const finalUV = corrected.add( 0.5 );
-
-	const inBounds = step( 0.0, finalUV.x ).mul( step( finalUV.x, 1.0 ) ).mul( step( 0.0, finalUV.y ) ).mul( step( finalUV.y, 1.0 ) );
-
-	return vec3( finalUV, inBounds );
-
-} );
+import { setConsoleFunction, REVISION } from 'three/webgpu';
 
 class Inspector extends RendererInspector {
 
@@ -43,7 +20,7 @@ class Inspector extends RendererInspector {
 
 		// init profiler
 
-		const profiler = new Profiler();
+		const profiler = new Profiler( this );
 		profiler.addEventListener( 'resize', ( e ) => this.dispatchEvent( e ) );
 
 		const parameters = new Parameters( {
@@ -81,7 +58,6 @@ class Inspector extends RendererInspector {
 		}
 
 		this.statsData = new Map();
-		this.canvasNodes = new Map();
 		this.profiler = profiler;
 		this.performance = performance;
 		this.memory = memory;
@@ -89,7 +65,9 @@ class Inspector extends RendererInspector {
 		this.parameters = parameters;
 		this.viewer = viewer;
 		this.timeline = timeline;
+		this.settings = settings;
 		this.once = {};
+		this.extensionsData = new WeakMap();
 
 		this.displayCycle = {
 			text: {
@@ -112,6 +90,34 @@ class Inspector extends RendererInspector {
 
 	}
 
+	onExtension( name, callback ) {
+
+		const extensionAdded = ( e ) => {
+
+			if ( e.name === name ) {
+
+				callback( e.tab );
+
+				this.settings.removeEventListener( 'extensionadded', extensionAdded );
+
+			}
+
+		};
+
+		if ( this.settings.extensions[ name ] && this.settings.extensions[ name ].loaded ) {
+
+			callback( this.settings.extensions[ name ] );
+
+		} else {
+
+			this.settings.addEventListener( 'extensionadded', extensionAdded );
+
+		}
+
+		return this;
+
+	}
+
 	hide() {
 
 		this.profiler.hide();
@@ -154,6 +160,14 @@ class Inspector extends RendererInspector {
 
 	}
 
+	setActiveExtension( name, value ) {
+
+		this.settings.setActiveExtension( name, value );
+
+		return this;
+
+	}
+
 	resolveConsoleOnce( type, message ) {
 
 		const key = type + message;
@@ -351,115 +365,62 @@ class Inspector extends RendererInspector {
 
 	}
 
-	getCanvasDataByNode( node ) {
-
-		let canvasData = this.canvasNodes.get( node );
-
-		if ( canvasData === undefined ) {
-
-			const renderer = this.getRenderer();
-
-			const canvas = document.createElement( 'canvas' );
-
-			const canvasTarget = new CanvasTarget( canvas );
-			canvasTarget.setPixelRatio( window.devicePixelRatio );
-			canvasTarget.setSize( 140, 140 );
-
-			const id = node.id;
-
-			const { path, name } = splitPath( splitCamelCase( node.getName() || '(unnamed)' ) );
+	getNodes() {
 
-			const target = node.context( { getUV: ( textureNode ) => {
-
-				const uvData = aspectRatioUV( screenUV, textureNode );
-				const correctedUV = uvData.xy;
-				const mask = uvData.z;
-
-				return correctedUV.mul( mask );
-
-			} } );
-
-			let output = vec4( vec3( target ), 1 );
-			output = renderOutput( output, NoToneMapping, renderer.outputColorSpace );
-			output = output.context( { inspector: true } );
-
-			const material = new NodeMaterial();
-			material.outputNode = output;
-
-			const quad = new QuadMesh( material );
-			quad.name = 'Viewer - ' + name;
-
-			canvasData = {
-				id,
-				name,
-				path,
-				node,
-				quad,
-				canvasTarget,
-				material
-			};
-
-			this.canvasNodes.set( node, canvasData );
-
-		}
-
-		return canvasData;
+		return this.currentNodes;
 
 	}
 
-	resolveViewer() {
-
-		const nodes = this.currentNodes;
-		const renderer = this.getRenderer();
+	getAverageDeltaTime( statsData, property, frames = this.fps ) {
 
-		if ( nodes.length === 0 ) return;
+		const statsArray = statsData.stats;
 
-		if ( ! renderer.backend.isWebGPUBackend ) {
+		let sum = 0;
+		let count = 0;
 
-			this.resolveConsoleOnce( 'warn', 'Inspector: Viewer is only available with WebGPU.' );
+		for ( let i = statsArray.length - 1; i >= 0 && count < frames; i -- ) {
 
-			return;
+			const stats = statsArray[ i ];
+			const value = stats[ property ];
 
-		}
+			if ( value > 0 ) {
 
-		//
+				// ignore invalid values
 
-		if ( ! this.viewer.isVisible ) {
+				sum += value;
+				count ++;
 
-			this.viewer.show();
+			}
 
 		}
 
-		const canvasDataList = nodes.map( node => this.getCanvasDataByNode( node ) );
-
-		this.viewer.update( renderer, canvasDataList );
+		return count > 0 ? sum / count : 0;
 
 	}
 
-	getAverageDeltaTime( statsData, property, frames = this.fps ) {
+	updateTabs() {
 
-		const statsArray = statsData.stats;
+		// tabs
 
-		let sum = 0;
-		let count = 0;
+		const tabs = Object.values( this.profiler.tabs );
 
-		for ( let i = statsArray.length - 1; i >= 0 && count < frames; i -- ) {
+		for ( const tab of tabs ) {
 
-			const stats = statsArray[ i ];
-			const value = stats[ property ];
+			let tabData = this.extensionsData.get( tab );
 
-			if ( value > 0 ) {
+			if ( tabData === undefined ) {
 
-				// ignore invalid values
+				tab.init( this );
 
-				sum += value;
-				count ++;
+				tabData = {};
+
+				this.extensionsData.set( tab, tabData );
 
 			}
 
-		}
+			tab.update( this );
 
-		return count > 0 ? sum / count : 0;
+		}
 
 	}
 
@@ -537,6 +498,45 @@ class Inspector extends RendererInspector {
 
 	}
 
+	static getItem( id ) {
+
+		console.warn( 'Inspector.getItem is deprecated. Use getItem directly instead.' );
+		return getItem( id );
+
+	}
+
+	static setItem( id, state ) {
+
+		console.warn( 'Inspector.setItem is deprecated. Use setItem directly instead.' );
+		setItem( id, state );
+
+	}
+
+}
+
+function getItem( id ) {
+
+	const data = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' );
+	return data[ id ] || {};
+
+}
+
+function setItem( id, state ) {
+
+	const data = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' );
+
+	if ( state === null ) {
+
+		delete data[ id ];
+
+	} else {
+
+		data[ id ] = state;
+
+	}
+
+	localStorage.setItem( 'threejs-inspector', JSON.stringify( data ) );
+
 }
 
-export { Inspector };
+export { Inspector, getItem, setItem };

+ 2 - 2
examples/jsm/inspector/RendererInspector.js

@@ -173,7 +173,7 @@ export class RendererInspector extends InspectorBase {
 
 	}
 
-	resolveViewer() { }
+	updateTabs() { }
 
 	resolveFrame( /*frame*/ ) { }
 
@@ -321,7 +321,7 @@ export class RendererInspector extends InspectorBase {
 
 		if ( this.isAvailable ) {
 
-			this.resolveViewer();
+			this.updateTabs();
 			this.resolveTimestamp();
 
 		}

+ 6 - 0
examples/jsm/inspector/extensions/extensions.json

@@ -0,0 +1,6 @@
+[
+	{
+		"name": "TSL Graph",
+		"url": "./tsl-graph/TSLGraphEditor.js"
+	}
+]

+ 175 - 9
examples/jsm/inspector/addons/tsl-graph/TSLGraphEditor.js → examples/jsm/inspector/extensions/tsl-graph/TSLGraphEditor.js

@@ -1,5 +1,5 @@
-import { error } from 'three/webgpu';
-import { Tab } from '../../ui/Tab.js';
+import { Raycaster, Vector2, BoxHelper, error, warn } from 'three/webgpu';
+import { Extension } from 'three/addons/inspector/Extension.js';
 import { TSLGraphLoader } from './TSLGraphLoader.js';
 
 const HOST_SOURCE = 'tsl-graph-host';
@@ -15,7 +15,7 @@ const _resposeByCommand = {
 
 const _refMaterials = new WeakMap();
 
-export class TSLGraphEditor extends Tab {
+class TSLGraphEditor extends Extension {
 
 	constructor( options = {} ) {
 
@@ -37,6 +37,7 @@ export class TSLGraphEditor extends Tab {
 		headerDiv.style.display = 'flex';
 		headerDiv.style.justifyContent = 'center';
 		headerDiv.style.gap = '4px';
+		headerDiv.style.position = 'relative';
 
 		const importBtn = document.createElement( 'button' );
 		importBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>';
@@ -59,17 +60,46 @@ export class TSLGraphEditor extends Tab {
 		manageBtn.style.padding = '5px 8px';
 		manageBtn.onclick = () => this._showManagerModal();
 
+		const autoIdBtn = document.createElement( 'button' );
+		autoIdBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3c.132 5.466 2.534 7.868 8 8-5.466.132-7.868 2.534-8 8-.132-5.466-2.534-7.868-8-8 5.466-.132 7.868-2.534 8-8z"></path></svg>';
+		autoIdBtn.className = 'panel-action-btn';
+		autoIdBtn.title = 'Auto-Generate Graph ID';
+		autoIdBtn.style.padding = '5px 8px';
+		autoIdBtn.style.position = 'absolute';
+		autoIdBtn.style.right = '4px';
+		autoIdBtn.style.top = '4px';
+
+		this.autoGraphId = false;
+
+		autoIdBtn.onclick = () => {
+
+			this.autoGraphId = ! this.autoGraphId;
+
+			if ( this.autoGraphId ) {
+
+				autoIdBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
+				autoIdBtn.style.color = '#fff';
+
+			} else {
+
+				autoIdBtn.style.backgroundColor = '';
+				autoIdBtn.style.color = '';
+
+			}
+
+		};
+
 		headerDiv.appendChild( importBtn );
 		headerDiv.appendChild( exportBtn );
 		headerDiv.appendChild( manageBtn );
+		headerDiv.appendChild( autoIdBtn );
 
 		this.content.appendChild( headerDiv );
 
 		this.iframe = document.createElement( 'iframe' );
 		this.iframe.style.width = '100%';
-		this.iframe.style.minHeight = '600px';
+		this.iframe.style.height = '100%';
 		this.iframe.style.border = 'none';
-		this.iframe.style.flex = '1';
 		this.iframe.src = editorUrl.toString();
 		this.editorOrigin = new URL( this.iframe.src ).origin;
 
@@ -102,6 +132,126 @@ export class TSLGraphEditor extends Tab {
 
 	}
 
+	_initPicker( inspector ) {
+
+		const renderer = inspector.getRenderer();
+
+		let boundingBox = null;
+
+		const raycaster = new Raycaster();
+		const pointer = new Vector2();
+
+		const removeBoundingBox = () => {
+
+			if ( boundingBox ) {
+
+				boundingBox.removeFromParent();
+				boundingBox.dispose();
+				boundingBox = null;
+
+			}
+
+		};
+
+		this.addEventListener( 'change', ( { material } ) => {
+
+			if ( material === null ) {
+
+				removeBoundingBox();
+
+			}
+
+		} );
+
+		this.addEventListener( 'remove', ( { graphId } ) => {
+
+			const frame = inspector.getFrame();
+			const scene = frame && frame.renders.length > 0 ? frame.renders[ 0 ].scene : null;
+
+			if ( scene ) {
+
+				scene.traverse( ( object ) => {
+
+					if ( object.material && object.material.userData && object.material.userData.graphId === graphId ) {
+
+						this.restoreMaterial( object.material );
+
+					}
+
+				} );
+
+			}
+
+		} );
+
+		const pointerDownPosition = new Vector2();
+
+		renderer.domElement.addEventListener( 'pointerdown', ( e ) => {
+
+			pointerDownPosition.set( e.clientX, e.clientY );
+
+		} );
+
+		renderer.domElement.addEventListener( 'pointerup', ( e ) => {
+
+			const frame = inspector.getFrame();
+
+			for ( const render of frame.renders ) {
+
+				const scene = render.scene;
+
+				if ( scene.isScene !== true ) continue;
+
+				const camera = render.camera;
+
+				if ( pointerDownPosition.distanceTo( pointer.set( e.clientX, e.clientY ) ) > 2 ) return;
+
+				const rect = renderer.domElement.getBoundingClientRect();
+				pointer.x = ( ( e.clientX - rect.left ) / rect.width ) * 2 - 1;
+				pointer.y = - ( ( e.clientY - rect.top ) / rect.height ) * 2 + 1;
+
+				raycaster.setFromCamera( pointer, camera );
+
+				const intersects = raycaster.intersectObjects( scene.children, true );
+
+				let graphMaterial = null;
+
+				if ( intersects.length > 0 ) {
+
+					for ( const intersect of intersects ) {
+
+						const object = intersect.object;
+						const material = object.material;
+
+						if ( material && material.isNodeMaterial ) {
+
+							removeBoundingBox();
+
+							boundingBox = new BoxHelper( object, 0xffff00 );
+							scene.add( boundingBox );
+
+							graphMaterial = material;
+
+						}
+
+						if ( object.isMesh || object.isSprite ) {
+
+							break;
+
+						}
+
+					}
+
+				}
+
+				this.setMaterial( graphMaterial );
+
+			}
+
+		} );
+
+	}
+
 	apply( scene ) {
 
 		const loader = new TSLGraphLoader();
@@ -119,6 +269,12 @@ export class TSLGraphEditor extends Tab {
 
 	}
 
+	init( inspector ) {
+
+		this._initPicker( inspector );
+
+	}
+
 	async setMaterial( material ) {
 
 		if ( this.material === material ) return;
@@ -269,7 +425,7 @@ export class TSLGraphEditor extends Tab {
 
 		if ( material.isNodeMaterial !== true ) {
 
-			error( 'Inspector: "Material" needs be a "NodeMaterial".' );
+			error( 'TSLGraphEditor: "Material" needs be a "NodeMaterial".' );
 
 			return;
 
@@ -277,9 +433,17 @@ export class TSLGraphEditor extends Tab {
 
 		if ( material.userData.graphId === undefined ) {
 
-			error( 'Inspector: "NodeMaterial" has no graphId. Set a "graphId" for the material in "material.userData.graphId".' );
+			if ( this.autoGraphId ) {
 
-			return;
+				material.userData.graphId = material.name || 'id:' + material.id;
+
+			} else {
+
+				warn( 'TSLGraphEditor: "NodeMaterial" has no graphId. Set a "graphId" for the material in "material.userData.graphId".' );
+
+				return;
+
+			}
 
 		}
 
@@ -625,7 +789,7 @@ export class TSLGraphEditor extends Tab {
 
 				} catch ( err ) {
 
-					error( 'TSLGraph: Failed to parse or load imported JSON.', err );
+					error( 'TSLGraphEditor: Failed to parse or load imported JSON.', err );
 
 				}
 
@@ -748,3 +912,5 @@ export class TSLGraphEditor extends Tab {
 	}
 
 }
+
+export default TSLGraphEditor;

+ 0 - 0
examples/jsm/inspector/addons/tsl-graph/TSLGraphLoader.js → examples/jsm/inspector/extensions/tsl-graph/TSLGraphLoader.js


+ 5 - 5
examples/jsm/inspector/tabs/Memory.js

@@ -79,7 +79,7 @@ class Memory extends Tab {
 		const memory = renderer.info.memory;
 
 		this.graph.addPoint( 'total', memory.total );
-		
+
 		if ( this.graph.limit === 0 ) this.graph.limit = 1;
 
 		this.graph.update();
@@ -98,17 +98,17 @@ class Memory extends Tab {
 		setText( this.attributes.data[ 1 ], memory.attributes.toString() );
 		setText( this.attributes.data[ 2 ], formatBytes( memory.attributesSize ) );
 		setText( this.geometries.data[ 1 ], memory.geometries.toString() );
-		
+
 		setText( this.indexAttributes.data[ 1 ], memory.indexAttributes.toString() );
 		setText( this.indexAttributes.data[ 2 ], formatBytes( memory.indexAttributesSize ) );
-		
+
 		setText( this.indirectStorageAttributes.data[ 1 ], memory.indirectStorageAttributes.toString() );
 		setText( this.indirectStorageAttributes.data[ 2 ], formatBytes( memory.indirectStorageAttributesSize ) );
 
 		setText( this.programs.data[ 1 ], memory.programs.toString() );
-		
+
 		setText( this.renderTargets.data[ 1 ], memory.renderTargets.toString() );
-		
+
 		setText( this.storageAttributes.data[ 1 ], memory.storageAttributes.toString() );
 		setText( this.storageAttributes.data[ 2 ], formatBytes( memory.storageAttributesSize ) );
 		setText( this.textures.data[ 1 ], memory.textures.toString() );

+ 182 - 41
examples/jsm/inspector/tabs/Settings.js

@@ -1,5 +1,8 @@
 import { Parameters } from './Parameters.js';
 import { WebGPURenderer, WebGLBackend, Node } from 'three/webgpu';
+import { getItem, setItem } from '../Inspector.js';
+
+const _EXTENSIONS_PATH = '../extensions/extensions.json';
 
 const _init = WebGPURenderer.prototype.init;
 
@@ -29,63 +32,48 @@ function forceWebGL( enable ) {
 
 }
 
-function loadState() {
-
-	let settings = {};
+let _state = null;
 
-	try {
+function _loadState() {
 
-		const data = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' );
-		settings = data.settings || {};
+	if ( _state !== null ) return _state;
 
-	} catch ( e ) {
+	const settings = getItem( 'settings' );
 
-		console.error( 'Failed to load settings:', e );
-
-	}
-
-	const state = {
-		forceWebGL: settings.forceWebGL || false,
-		captureStackTrace: settings.captureStackTrace || false
+	_state = {
+		forceWebGL: settings.forceWebGL !== undefined ? settings.forceWebGL : false,
+		captureStackTrace: settings.captureStackTrace !== undefined ? settings.captureStackTrace : false,
+		activeExtensions: settings.activeExtensions !== undefined ? settings.activeExtensions : {}
 	};
 
-	return state;
-
-}
+	if ( _state.forceWebGL ) {
 
-function saveState( state ) {
-
-	try {
-
-		const data = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' );
-		data.settings = state;
-
-		localStorage.setItem( 'threejs-inspector', JSON.stringify( data ) );
-
-	} catch ( e ) {
-
-		console.error( 'Failed to save settings:', e );
+		forceWebGL( true );
 
 	}
 
-}
+	if ( _state.captureStackTrace ) {
 
-//
-
-const state = loadState();
+		Node.captureStackTrace = true;
 
-if ( state.forceWebGL ) {
+	}
 
-	forceWebGL( true );
+	return _state;
 
 }
 
-if ( state.captureStackTrace ) {
+function _saveState() {
 
-	Node.captureStackTrace = true;
+	setItem( 'settings', {
+		forceWebGL: _state.forceWebGL,
+		captureStackTrace: _state.captureStackTrace,
+		activeExtensions: _state.activeExtensions
+	} );
 
 }
 
+_loadState();
+
 //
 
 class Settings extends Parameters {
@@ -94,23 +82,27 @@ class Settings extends Parameters {
 
 		super( { name: 'Settings' } );
 
+		this.extensions = {};
+
+		const currentState = _loadState();
+
 		// UI
 
 		const rendererGroup = this.createGroup( 'Renderer' );
 
-		rendererGroup.add( state, 'forceWebGL' ).name( 'Force WebGL' ).onChange( ( enable ) => {
+		rendererGroup.add( currentState, 'forceWebGL' ).name( 'Force WebGL' ).onChange( ( enable ) => {
 
 			forceWebGL( enable );
-			saveState( state );
+			_saveState();
 
 			location.reload();
 
 		} );
 
-		rendererGroup.add( state, 'captureStackTrace' ).name( 'Capture Stack Trace' ).onChange( ( enable ) => {
+		rendererGroup.add( currentState, 'captureStackTrace' ).name( 'Capture Stack Trace' ).onChange( ( enable ) => {
 
 			Node.captureStackTrace = enable;
-			saveState( state );
+			_saveState();
 
 			location.reload();
 
@@ -118,6 +110,155 @@ class Settings extends Parameters {
 
 	}
 
+	init() {
+
+		const extensionsGroup = this.createGroup( 'Extensions' );
+
+		this._getExtensions().then( extensions => {
+
+			for ( const extension of extensions ) {
+
+				extension.active = false;
+				extension.loaded = false;
+				extension.tab = null;
+
+				this.extensions[ extension.name ] = extension;
+
+				extension.ui = extensionsGroup.add( { [ extension.name ]: false }, extension.name ).onChange( async ( value ) => {
+
+					this.setActiveExtension( extension.name, value );
+
+					// User preference
+
+					if ( value ) {
+
+						_state.activeExtensions[ extension.name ] = {
+							name: extension.name,
+							url: extension.url
+						};
+
+					} else {
+
+						delete _state.activeExtensions[ extension.name ];
+
+
+					}
+
+					//
+
+					this._updateExtensionUI( extension );
+
+					_saveState();
+
+				} );
+
+				// Set user-defined state
+
+				if ( _state.activeExtensions[ extension.name ] !== undefined ) {
+
+					extension.ui.setValue( true );
+
+				}
+
+			}
+
+		} );
+
+	}
+
+	async setActiveExtension( name, value ) {
+
+		const extension = this.extensions[ name ];
+		const inspector = this.inspector;
+
+		if ( extension ) {
+
+			if ( value ) {
+
+				await this._loadExtension( inspector, extension );
+
+			} else {
+
+				await this._unloadExtension( inspector, extension );
+
+			}
+
+		}
+
+	}
+
+	_updateExtensionUI( extension ) {
+
+		const forceActive = extension.active && _state.activeExtensions[ extension.name ] === undefined;
+
+		if ( forceActive ) {
+
+			extension.ui.checkbox.checked = true;
+			extension.ui.domElement.style.setProperty( '--accent-color', 'var(--color-green)' );
+
+		} else {
+
+			extension.ui.domElement.style.removeProperty( '--accent-color' );
+
+		}
+
+	}
+
+	async _unloadExtension( inspector, extension ) {
+
+		if ( extension.active === false ) return;
+
+		//
+
+		inspector.removeTab( extension.tab );
+
+		extension.active = false;
+		extension.loaded = false;
+		extension.tab = null;
+
+		this._updateExtensionUI( extension );
+
+		this.dispatchEvent( { type: 'extensionremoved', name: extension.name } );
+
+	}
+
+	async _loadExtension( inspector, extension ) {
+
+		if ( extension.active === true ) return;
+
+		//
+
+		extension.active = true;
+
+		const extUrl = new URL( extension.url, new URL( _EXTENSIONS_PATH, import.meta.url ) ).href;
+
+		const module = await import( extUrl );
+
+		const keys = Object.keys( module );
+		const ExtensionClass = module[ keys[ 0 ] ];
+		const extensionTab = new ExtensionClass();
+
+		inspector.addTab( extensionTab );
+
+		extension.loaded = true;
+		extension.tab = extensionTab;
+
+		this._updateExtensionUI( extension );
+
+		this.dispatchEvent( { type: 'extensionadded', name: extension.name, tab: extensionTab } );
+
+	}
+
+	async _getExtensions() {
+
+		const url = new URL( _EXTENSIONS_PATH, import.meta.url );
+
+		const extensions = await fetch( url ).then( res => res.json() );
+
+		return extensions;
+
+	}
+
 }
 
 export { Settings };

+ 8 - 8
examples/jsm/inspector/tabs/Timeline.js

@@ -1,5 +1,6 @@
 import { Tab } from '../ui/Tab.js';
 import { Graph } from '../ui/Graph.js';
+import { getItem, setItem } from '../Inspector.js';
 
 const LIMIT = 500;
 
@@ -93,10 +94,9 @@ class Timeline extends Tab {
 		this.recordRefreshButton.style.alignItems = 'center';
 		this.recordRefreshButton.addEventListener( 'click', () => {
 
-			const storage = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' );
-			storage.timeline = storage.timeline || {};
-			storage.timeline.recording = true;
-			localStorage.setItem( 'threejs-inspector', JSON.stringify( storage ) );
+			const timelineSettings = getItem( 'timeline' );
+			timelineSettings.recording = true;
+			setItem( 'timeline', timelineSettings );
 
 			window.location.reload();
 
@@ -442,12 +442,12 @@ class Timeline extends Tab {
 
 		this.renderer = renderer;
 
-		const storage = JSON.parse( localStorage.getItem( 'threejs-inspector' ) || '{}' );
+		const timelineSettings = getItem( 'timeline' );
 
-		if ( storage.timeline && storage.timeline.recording ) {
+		if ( timelineSettings.recording ) {
 
-			storage.timeline.recording = false;
-			localStorage.setItem( 'threejs-inspector', JSON.stringify( storage ) );
+			timelineSettings.recording = false;
+			setItem( 'timeline', timelineSettings );
 
 			this.toggleRecording();
 

+ 103 - 3
examples/jsm/inspector/tabs/Viewer.js

@@ -1,8 +1,32 @@
 import { Tab } from '../ui/Tab.js';
 import { List } from '../ui/List.js';
 import { Item } from '../ui/Item.js';
+import { splitPath, splitCamelCase } from '../ui/utils.js';
 
-import { RendererUtils, NoToneMapping, LinearSRGBColorSpace } from 'three/webgpu';
+import { RendererUtils, NoToneMapping, LinearSRGBColorSpace, QuadMesh, NodeMaterial, CanvasTarget } from 'three/webgpu';
+import { renderOutput, vec2, vec3, vec4, Fn, screenUV, step, OnMaterialUpdate, uniform } from 'three/tsl';
+
+const aspectRatioUV = /*@__PURE__*/ Fn( ( [ uv, textureNode ] ) => {
+
+	const aspect = uniform( 0 );
+
+	OnMaterialUpdate( () => {
+
+		const { width, height } = textureNode.value;
+
+		aspect.value = width / height;
+
+	} );
+
+	const centered = uv.sub( 0.5 );
+	const corrected = vec2( centered.x.div( aspect ), centered.y );
+	const finalUV = corrected.add( 0.5 );
+
+	const inBounds = step( 0.0, finalUV.x ).mul( step( finalUV.x, 1.0 ) ).mul( step( 0.0, finalUV.y ) ).mul( step( finalUV.y, 1.0 ) );
+
+	return vec3( finalUV, inBounds );
+
+} );
 
 class Viewer extends Tab {
 
@@ -26,6 +50,7 @@ class Viewer extends Tab {
 
 		this.itemLibrary = new Map();
 		this.folderLibrary = new Map();
+		this.canvasNodes = new Map();
 		this.currentDataList = [];
 		this.nodeList = nodeList;
 		this.nodes = nodes;
@@ -68,9 +93,84 @@ class Viewer extends Tab {
 
 	}
 
-	update( renderer, canvasDataList ) {
+	getCanvasDataByNode( renderer, node ) {
+
+		let canvasData = this.canvasNodes.get( node );
+
+		if ( canvasData === undefined ) {
+
+			const canvas = document.createElement( 'canvas' );
+
+			const canvasTarget = new CanvasTarget( canvas );
+			canvasTarget.setPixelRatio( window.devicePixelRatio );
+			canvasTarget.setSize( 140, 140 );
+
+			const id = node.id;
+
+			const { path, name } = splitPath( splitCamelCase( node.getName() || '(unnamed)' ) );
+
+			const target = node.context( { getUV: ( textureNode ) => {
+
+				const uvData = aspectRatioUV( screenUV, textureNode );
+				const correctedUV = uvData.xy;
+				const mask = uvData.z;
+
+				return correctedUV.mul( mask );
+
+			} } );
+
+			let output = vec4( vec3( target ), 1 );
+			output = renderOutput( output, NoToneMapping, renderer.outputColorSpace );
+			output = output.context( { inspector: true } );
+
+			const material = new NodeMaterial();
+			material.outputNode = output;
+
+			const quad = new QuadMesh( material );
+			quad.name = 'Viewer - ' + name;
+
+			canvasData = {
+				id,
+				name,
+				path,
+				node,
+				quad,
+				canvasTarget,
+				material
+			};
+
+			this.canvasNodes.set( node, canvasData );
+
+		}
+
+		return canvasData;
+
+	}
+
+	update( inspector ) {
+
+		const renderer = inspector.getRenderer();
+		const nodes = inspector.getNodes();
+
+		if ( nodes.length > 0 ) {
+
+			if ( ! renderer.backend.isWebGPUBackend ) {
+
+				inspector.resolveConsoleOnce( 'warn', 'Inspector: Viewer is only available with WebGPU.' );
+
+				return;
+
+			}
+
+			if ( ! this.isVisible ) {
+
+				this.show();
+
+			}
+
+		}
 
-		if ( ! this.isActive && ! this.isDetached ) return;
+		const canvasDataList = nodes.map( node => this.getCanvasDataByNode( renderer, node ) );
 
 		//
 

+ 101 - 14
examples/jsm/inspector/ui/Profiler.js

@@ -1,12 +1,14 @@
 import { EventDispatcher } from 'three';
 import { Style } from './Style.js';
+import { getItem, setItem } from '../Inspector.js';
 
 export class Profiler extends EventDispatcher {
 
-	constructor() {
+	constructor( inspector ) {
 
 		super();
 
+		this.inspector = inspector;
 		this.tabs = {};
 		this.activeTabId = null;
 		this.isResizing = false;
@@ -517,6 +519,9 @@ export class Profiler extends EventDispatcher {
 		// Update panel size when tabs change
 		this.updatePanelSize();
 
+		// Set profiler reference
+		tab.profiler = this;
+
 	}
 
 	addBuiltinTab( tab ) {
@@ -573,7 +578,6 @@ export class Profiler extends EventDispatcher {
 		// Store references
 		tab.builtinButton = builtinButton;
 		tab.miniContent = miniContent;
-		tab.profiler = this;
 
 		// If the tab was hidden before being added, hide the builtin button
 		if ( ! tab.isVisible ) {
@@ -595,6 +599,98 @@ export class Profiler extends EventDispatcher {
 
 	}
 
+	removeTab( tab ) {
+
+		if ( ! tab || this.tabs[ tab.id ] === undefined ) return;
+
+		delete this.tabs[ tab.id ];
+
+		if ( tab.isDetached && tab.detachedWindow ) {
+
+			if ( tab.detachedWindow.panel && tab.detachedWindow.panel.parentNode ) {
+
+				tab.detachedWindow.panel.parentNode.removeChild( tab.detachedWindow.panel );
+
+			}
+
+			const index = this.detachedWindows.indexOf( tab.detachedWindow );
+
+			if ( index !== - 1 ) {
+
+				this.detachedWindows.splice( index, 1 );
+
+			}
+
+		}
+
+		if ( ! tab.builtin ) {
+
+			if ( tab.button && tab.button.parentNode ) {
+
+				tab.button.parentNode.removeChild( tab.button );
+
+			}
+
+		} else {
+
+			if ( tab.builtinButton && tab.builtinButton.parentNode ) {
+
+				tab.builtinButton.parentNode.removeChild( tab.builtinButton );
+
+			}
+
+			if ( tab.miniContent && tab.miniContent.parentNode ) {
+
+				tab.miniContent.parentNode.removeChild( tab.miniContent );
+
+			}
+
+			// Clean up builtin container if empty
+			const hasVisibleBuiltinButtons = Array.from( this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ) )
+				.some( btn => btn.style.display !== 'none' );
+
+			if ( ! hasVisibleBuiltinButtons ) {
+
+				this.builtinTabsContainer.style.display = 'none';
+
+			}
+
+		}
+
+		if ( tab.content && tab.content.parentNode ) {
+
+			tab.content.parentNode.removeChild( tab.content );
+
+		}
+
+		if ( this.activeTabId === tab.id ) {
+
+			this.activeTabId = null;
+
+			// Try to activate another tab
+			const remainingTabs = Object.values( this.tabs ).filter( t => ! t.isDetached && t.isVisible );
+
+			if ( remainingTabs.length > 0 ) {
+
+				this.setActiveTab( remainingTabs[ 0 ].id );
+
+			} else {
+
+				this.updatePanelSize();
+
+			}
+
+		} else {
+
+			this.updatePanelSize();
+
+		}
+
+		tab.onVisibilityChange = null;
+		tab.profiler = null;
+
+	}
+
 	updatePanelSize() {
 
 		// Check if there are any visible tabs in the panel
@@ -1643,11 +1739,7 @@ export class Profiler extends EventDispatcher {
 
 		try {
 
-			const savedData = localStorage.getItem( 'threejs-inspector' );
-			const data = JSON.parse( savedData || '{}' );
-
-			data.layout = layout;
-			localStorage.setItem( 'threejs-inspector', JSON.stringify( data ) );
+			setItem( 'layout', layout );
 
 		} catch ( e ) {
 
@@ -1663,14 +1755,9 @@ export class Profiler extends EventDispatcher {
 
 		try {
 
-			const savedData = localStorage.getItem( 'threejs-inspector' );
-
-			if ( ! savedData ) return;
-
-			const parsedData = JSON.parse( savedData );
-			const layout = parsedData.layout;
+			const layout = getItem( 'layout' );
 
-			if ( ! layout ) return;
+			if ( Object.keys( layout ).length === 0 ) return;
 
 			// Constrain detached tabs positions to current screen bounds
 			if ( layout.detachedTabs && layout.detachedTabs.length > 0 ) {

+ 10 - 0
examples/jsm/inspector/ui/Tab.js

@@ -54,6 +54,16 @@ export class Tab extends EventDispatcher {
 
 	}
 
+	get inspector() {
+
+		return this.profiler.inspector;
+
+	}
+
+	init( /*inspector*/ ) { }
+
+	update( /*inspector*/ ) { }
+
 	setActive( isActive ) {
 
 		this.button.classList.toggle( 'active', isActive );

+ 1 - 1
examples/jsm/inspector/ui/Values.js

@@ -204,7 +204,7 @@ class ValueCheckbox extends Value {
 
 	setValue( val ) {
 
-		this.checkbox.value = val;
+		this.checkbox.checked = val;
 
 		return super.setValue( val );
 

+ 17 - 111
examples/webgpu_tsl_graph.html

@@ -49,8 +49,7 @@
 
 			import { Inspector } from 'three/addons/inspector/Inspector.js';
 
-			import { TSLGraphLoader } from 'three/addons/inspector/addons/tsl-graph/TSLGraphLoader.js';
-			import { TSLGraphEditor } from 'three/addons/inspector/addons/tsl-graph/TSLGraphEditor.js';
+			import { TSLGraphLoader } from 'three/addons/inspector/extensions/tsl-graph/TSLGraphLoader.js';
 
 			let camera, scene, renderer;
 			let controls;
@@ -60,13 +59,6 @@
 
 			async function initTSLGraph() {
 
-				// TSL Graph Editor
-
-				const tslGraph = new TSLGraphEditor();
-
-				renderer.inspector.addTab( tslGraph );
-				renderer.inspector.setActiveTab( tslGraph );
-
 				// Create Materials
 
 				const m1 = new THREE.MeshPhysicalNodeMaterial();
@@ -89,123 +81,37 @@
 
 				}
 
-				// Initialize
-
-				// Load and apply TSL Graph from a file or from Local Storage if exists
-				// Every time a TSL Graph is changed, it will be stored in the local storage
-
-				if ( tslGraph.hasGraphs ) {
-
-					tslGraph.apply( scene );
-
-				} else {
-
-					// Load a TSL Graph from a file
-
-					const tslLoader = new TSLGraphLoader();
-					const applier = await tslLoader.setPath( './shaders/' ).loadAsync( 'tsl-graphs.json' );
-
-					applier.apply( scene );
-
-				}
-
-				// Picker a Material
+				// TSL Graph Editor
 
-				let boundingBox = null;
+				renderer.inspector.onExtension( 'TSL Graph', async ( tslGraph ) => {
 
-				const raycaster = new THREE.Raycaster();
-				const pointer = new THREE.Vector2();
+					renderer.inspector.setActiveTab( tslGraph );
 
-				function removeBoundingBox() {
+					// Apply TSL Graph from Local Storage if exists
+					// Every time a TSL Graph is changed, it will be stored in the local storage
 
-					scene.remove( boundingBox );
-					boundingBox.dispose();
+					if ( tslGraph.hasGraphs ) {
 
-				}
+						tslGraph.apply( scene );
 
-				tslGraph.addEventListener( 'change', ( { material } ) => {
+					} else {
 
-					if ( material === null && boundingBox ) {
+						// Load a TSL Graph from a file
+						// Use it for production
 
-						removeBoundingBox();
+						const tslLoader = new TSLGraphLoader();
+						const applier = await tslLoader.setPath( './shaders/' ).loadAsync( 'tsl-graphs.json' );
 
-						boundingBox = null;
+						applier.apply( scene );
 
 					}
 
 				} );
 
-				tslGraph.addEventListener( 'remove', ( { graphId } ) => {
-
-					scene.traverse( ( object ) => {
-
-						if ( object.material && object.material.userData && object.material.userData.graphId === graphId ) {
-
-							tslGraph.restoreMaterial( object.material );
-
-						}
-
-					} );
-
-				} );
-
-				const pointerDownPosition = new THREE.Vector2();
-
-				renderer.domElement.addEventListener( 'pointerdown', ( e ) => {
-
-					pointerDownPosition.set( e.clientX, e.clientY );
+				// Active TSL Graph Editor
+				// Only is needed if you don't activate it from the GUI
 
-				} );
-
-				renderer.domElement.addEventListener( 'pointerup', ( e ) => {
-
-					if ( pointerDownPosition.distanceTo( pointer.set( e.clientX, e.clientY ) ) > 2 ) return;
-
-					const rect = renderer.domElement.getBoundingClientRect();
-					pointer.x = ( ( e.clientX - rect.left ) / rect.width ) * 2 - 1;
-					pointer.y = - ( ( e.clientY - rect.top ) / rect.height ) * 2 + 1;
-
-					raycaster.setFromCamera( pointer, camera );
-
-					const intersects = raycaster.intersectObjects( scene.children, true );
-
-					let graphMaterial = null;
-
-					if ( intersects.length > 0 ) {
-
-						for ( const intersect of intersects ) {
-
-							const object = intersect.object;
-							const material = object.material;
-
-							if ( material.userData && material.userData.graphId ) {
-
-								if ( boundingBox ) {
-
-									removeBoundingBox();
-
-								}
-
-								boundingBox = new THREE.BoxHelper( object, 0xffff00 );
-								scene.add( boundingBox );
-
-								graphMaterial = material;
-
-							}
-
-							if ( object.isMesh || object.isSprite ) {
-
-								break;
-
-							}
-
-						}
-
-					}
-
-					tslGraph.setMaterial( graphMaterial );
-
-				} );
+				renderer.inspector.setActiveExtension( 'TSL Graph', true );
 
 			}
 

粤ICP备19079148号