| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878 |
- 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 { getItem, setItem } from '../Inspector.js';
- import { RendererUtils, NoToneMapping, LinearSRGBColorSpace, QuadMesh, NodeMaterial, CanvasTarget, Vector2 } from 'three/webgpu';
- import { renderOutput, vec2, vec3, vec4, Fn, screenUV, step, OnMaterialUpdate, uniform, float } from 'three/tsl';
- const _size = /*@__PURE__*/ new Vector2();
- const aspectRatioUV = /*@__PURE__*/ Fn( ( [ uv, textureNode, canvasAspect ] ) => {
- const textureAspect = uniform( 0 );
- OnMaterialUpdate( () => {
- const { width, height } = textureNode.value;
- textureAspect.value = width / height;
- } );
- const ratio = canvasAspect.div( textureAspect );
- const centered = uv.sub( 0.5 );
- // If canvasAspect > textureAspect:
- const uvWide = vec2( centered.x.mul( ratio ), centered.y ).add( 0.5 );
- // If canvasAspect <= textureAspect:
- const uvTall = vec2( centered.x, centered.y.div( ratio ) ).add( 0.5 );
- const finalUV = canvasAspect.greaterThan( textureAspect ).select( uvWide, uvTall );
- 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 {
- constructor( options = {} ) {
- super( 'Viewer', options );
- this.content.style.overflow = 'hidden';
- this.maximizedByFullscreenButton = false;
- // Toolbar
- const toolbar = document.createElement( 'div' );
- toolbar.className = 'toolbar';
- const backBtn = document.createElement( 'button' );
- backBtn.className = 'viewer-back-btn';
- backBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>';
- backBtn.title = 'Back to list';
- backBtn.style.display = 'none';
- toolbar.appendChild( backBtn );
- const label = document.createElement( 'span' );
- label.textContent = 'View:';
- toolbar.appendChild( label );
- const select = document.createElement( 'select' );
- select.className = 'select';
- select.style.width = '200px';
- const defaultOption = document.createElement( 'option' );
- defaultOption.value = 'list';
- defaultOption.textContent = 'List';
- select.appendChild( defaultOption );
- toolbar.appendChild( select );
- this.content.appendChild( toolbar );
- const nodeList = new List( 'Viewer', 'Name' );
- nodeList.setGridStyle( '150px minmax(200px, 2fr)' );
- nodeList.domElement.style.minWidth = '400px';
- const scrollWrapper = document.createElement( 'div' );
- scrollWrapper.className = 'list-scroll-wrapper';
- scrollWrapper.style.flexGrow = '1';
- scrollWrapper.style.overflowY = 'auto';
- scrollWrapper.style.minHeight = '0';
- scrollWrapper.appendChild( nodeList.domElement );
- this.content.appendChild( scrollWrapper );
- // Container for full screen view
- const fullViewerContainer = document.createElement( 'div' );
- fullViewerContainer.className = 'full-viewer-container';
- fullViewerContainer.style.touchAction = 'none';
- this.content.appendChild( fullViewerContainer );
- const nodes = new Item( 'User Defined' );
- nodeList.add( nodes );
- //
- this.itemLibrary = new Map();
- this.folderLibrary = new Map();
- this.canvasNodes = new Map();
- this.currentDataList = [];
- this.nodeList = nodeList;
- this.nodes = nodes;
- this.scrollWrapper = scrollWrapper;
- this.fullViewerContainer = fullViewerContainer;
- this.select = select;
- this.backBtn = backBtn;
- this.activeFullNodeId = null;
- this.pendingRestoreView = true;
- backBtn.addEventListener( 'click', () => {
- select.value = 'list';
- this.showListView();
- if ( this.maximizedByFullscreenButton ) {
- if ( this.profiler && this.profiler.panel.classList.contains( 'maximized' ) ) {
- this.profiler.toggleMaximize();
- }
- this.maximizedByFullscreenButton = false;
- }
- this.saveLastView();
- } );
- select.addEventListener( 'change', () => {
- const val = select.value;
- if ( val === 'list' ) {
- this.showListView();
- } else {
- this.showNodeView( val );
- }
- this.saveLastView();
- } );
- // Event forwarding setup for OrbitControls
- this.isDraggingThumbnail = false;
- this.activeSourceCanvas = null;
- this.activePointerIds = new Set();
- const handleGlobalPointer = ( e ) => {
- if ( ! this.isDraggingThumbnail || ! this.activeSourceCanvas ) return;
- const renderer = this.inspector.getRenderer();
- if ( ! renderer || ! renderer.domElement ) return;
- if ( e.isForwarded ) return;
- // Block native event from reaching other document-level listeners (OrbitControls)
- e.stopImmediatePropagation();
- e.preventDefault();
- // Project and dispatch forwarded event
- this.forwardEvent( e, this.activeSourceCanvas, renderer.domElement );
- if ( e.type === 'pointerup' || e.type === 'pointercancel' ) {
- this.activePointerIds.delete( e.pointerId );
- if ( this.activePointerIds.size === 0 ) {
- this.isDraggingThumbnail = false;
- this.activeSourceCanvas = null;
- }
- }
- };
- window.addEventListener( 'pointermove', handleGlobalPointer, true );
- window.addEventListener( 'pointerup', handleGlobalPointer, true );
- window.addEventListener( 'pointercancel', handleGlobalPointer, true );
- }
- getFolder( name ) {
- let folder = this.folderLibrary.get( name );
- if ( folder === undefined ) {
- folder = new Item( name );
- this.folderLibrary.set( name, folder );
- this.nodeList.add( folder );
- }
- return folder;
- }
- hide() {
- super.hide();
- this.maximizedByFullscreenButton = false;
- this.isDraggingThumbnail = false;
- this.activeSourceCanvas = null;
- this.activePointerIds.clear();
- }
- addNodeItem( canvasData ) {
- let item = this.itemLibrary.get( canvasData.id );
- if ( item === undefined ) {
- const name = canvasData.name;
- const domElement = canvasData.canvasTarget.domElement;
- // Create wrapper
- const wrapper = document.createElement( 'div' );
- wrapper.className = 'node-canvas-wrapper';
- wrapper.style.position = 'relative';
- wrapper.style.display = 'inline-block';
- wrapper.style.width = '140px';
- wrapper.style.height = '140px';
- wrapper.style.touchAction = 'none';
- // View full screen button
- const viewBtn = document.createElement( 'button' );
- viewBtn.className = 'node-canvas-detach-btn';
- viewBtn.title = 'View full size';
- viewBtn.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"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>';
- viewBtn.onclick = ( e ) => {
- e.stopPropagation();
- this.select.value = canvasData.id;
- this.showNodeView( canvasData.id );
- this.saveLastView();
- };
- // Fullscreen and maximize button
- const fullscreenBtn = document.createElement( 'button' );
- fullscreenBtn.className = 'node-canvas-fullscreen-btn';
- fullscreenBtn.title = 'Fullscreen view';
- fullscreenBtn.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="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
- fullscreenBtn.onclick = ( e ) => {
- e.stopPropagation();
- this.select.value = canvasData.id;
- this.showNodeView( canvasData.id );
- if ( this.profiler && ! this.profiler.panel.classList.contains( 'maximized' ) ) {
- this.profiler.toggleMaximize();
- this.maximizedByFullscreenButton = true;
- if ( ! this._maximizeListenerAdded && this.profiler.maximizeBtn ) {
- this.profiler.maximizeBtn.addEventListener( 'click', () => {
- this.maximizedByFullscreenButton = false;
- } );
- this._maximizeListenerAdded = true;
- }
- }
- this.saveLastView();
- };
- wrapper.appendChild( domElement );
- wrapper.appendChild( viewBtn );
- wrapper.appendChild( fullscreenBtn );
- this.setupEventForwarding( domElement );
- // Store elements in canvasData for access
- canvasData.domElement = domElement;
- canvasData.wrapperElement = wrapper;
- item = new Item( wrapper, name );
- item.itemRow.children[ 1 ].style[ 'justify-content' ] = 'flex-start';
- this.itemLibrary.set( canvasData.id, item );
- }
- return item;
- }
- setupEventForwarding( sourceCanvas ) {
- sourceCanvas.style.touchAction = 'none';
- const onPointerDown = ( e ) => {
- const renderer = this.inspector.getRenderer();
- if ( ! renderer || ! renderer.domElement ) return;
- const targetCanvas = renderer.domElement;
- this.isDraggingThumbnail = true;
- this.activeSourceCanvas = sourceCanvas;
- this.activePointerIds.add( e.pointerId );
- // Project and dispatch pointerdown
- this.forwardEvent( e, sourceCanvas, targetCanvas );
- };
- sourceCanvas.addEventListener( 'pointerdown', onPointerDown );
- // Wheel event support for zooming
- const onWheel = ( e ) => {
- const renderer = this.inspector.getRenderer();
- if ( ! renderer || ! renderer.domElement ) return;
- e.preventDefault();
- e.stopPropagation();
- this.forwardEvent( e, sourceCanvas, renderer.domElement );
- };
- sourceCanvas.addEventListener( 'wheel', onWheel, { passive: false } );
- // Click, dblclick, contextmenu
- const onMouseShortcut = ( e ) => {
- const renderer = this.inspector.getRenderer();
- if ( ! renderer || ! renderer.domElement ) return;
- e.stopPropagation();
- if ( e.type === 'contextmenu' ) {
- e.preventDefault();
- }
- this.forwardEvent( e, sourceCanvas, renderer.domElement );
- };
- sourceCanvas.addEventListener( 'click', onMouseShortcut );
- sourceCanvas.addEventListener( 'dblclick', onMouseShortcut );
- sourceCanvas.addEventListener( 'contextmenu', onMouseShortcut );
- }
- forwardEvent( event, sourceCanvas, targetCanvas ) {
- const sourceRect = sourceCanvas.getBoundingClientRect();
- const targetRect = targetCanvas.getBoundingClientRect();
- const localX = ( event.clientX - sourceRect.left ) / sourceRect.width;
- const localY = ( event.clientY - sourceRect.top ) / sourceRect.height;
- const targetClientX = targetRect.left + localX * targetRect.width;
- const targetClientY = targetRect.top + localY * targetRect.height;
- const targetPageX = targetClientX + window.scrollX;
- const targetPageY = targetClientY + window.scrollY;
- let newEvent;
- const eventInit = {
- bubbles: true,
- cancelable: true,
- view: window,
- clientX: targetClientX,
- clientY: targetClientY,
- screenX: targetClientX + window.screenX,
- screenY: targetClientY + window.screenY,
- pageX: targetPageX,
- pageY: targetPageY,
- ctrlKey: event.ctrlKey,
- shiftKey: event.shiftKey,
- altKey: event.altKey,
- metaKey: event.metaKey,
- buttons: event.buttons,
- button: event.button
- };
- if ( event instanceof WheelEvent ) {
- newEvent = new WheelEvent( event.type, {
- ...eventInit,
- deltaX: event.deltaX,
- deltaY: event.deltaY,
- deltaZ: event.deltaZ,
- deltaMode: event.deltaMode
- } );
- } else if ( window.PointerEvent && event instanceof PointerEvent ) {
- newEvent = new PointerEvent( event.type, {
- ...eventInit,
- pointerId: event.pointerId,
- width: event.width,
- height: event.height,
- pressure: event.pressure,
- tiltX: event.tiltX,
- tiltY: event.tiltY,
- pointerType: event.pointerType,
- isPrimary: event.isPrimary
- } );
- } else {
- newEvent = new MouseEvent( event.type, eventInit );
- }
- newEvent.isForwarded = true;
- targetCanvas.dispatchEvent( newEvent );
- }
- showListView() {
- if ( this.activeFullNodeId ) {
- const canvasData = Array.from( this.canvasNodes.values() ).find( data => String( data.id ) === String( this.activeFullNodeId ) );
- if ( canvasData ) {
- // Move canvas back to wrapper
- canvasData.wrapperElement.appendChild( canvasData.domElement );
- // Reset size
- canvasData.domElement.style.width = '';
- canvasData.domElement.style.height = '';
- canvasData.canvasTarget.setSize( 140, 140 );
- const renderer = this.inspector.getRenderer();
- renderer.backend.delete( canvasData.canvasTarget );
- }
- this.activeFullNodeId = null;
- }
- this.scrollWrapper.style.display = '';
- this.fullViewerContainer.style.display = 'none';
- this.backBtn.style.display = 'none';
- }
- showNodeView( nodeId ) {
- // First restore previous full screen node if any
- if ( this.activeFullNodeId && String( this.activeFullNodeId ) !== String( nodeId ) ) {
- this.showListView();
- }
- const canvasData = Array.from( this.canvasNodes.values() ).find( data => String( data.id ) === String( nodeId ) );
- if ( canvasData ) {
- this.addNodeItem( canvasData );
- this.activeFullNodeId = nodeId;
- this.backBtn.style.display = 'flex';
- // Hide list, show full screen container
- this.scrollWrapper.style.display = 'none';
- this.fullViewerContainer.style.display = 'flex';
- // Move canvas to the full viewer container
- this.fullViewerContainer.appendChild( canvasData.domElement );
- canvasData.domElement.style.width = '100%';
- canvasData.domElement.style.height = '100%';
- // Resize canvas to fit full viewer container
- const rect = this.fullViewerContainer.getBoundingClientRect();
- const contentWidth = rect.width || this.content.clientWidth;
- const contentHeight = rect.height || ( this.content.clientHeight - 38 ); // minus toolbar
- canvasData.canvasTarget.setSize( contentWidth, contentHeight );
- const renderer = this.inspector.getRenderer();
- renderer.backend.delete( canvasData.canvasTarget );
- }
- }
- 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 canvasAspect = uniform( 1 );
- const mask = float( 1 );
- const target = node.context( { getUV: ( textureNode ) => {
- const uvData = aspectRatioUV( screenUV, textureNode, canvasAspect );
- const correctedUV = uvData.xy;
- mask.assign( uvData.z );
- return correctedUV;
- } } );
- let output = vec4( vec3( target ), 1 ).mul( mask );
- 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,
- canvasAspect
- };
- 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 ) return;
- const canvasDataList = nodes.map( node => this.getCanvasDataByNode( renderer, node ) );
- // Check if the list of nodes has changed
- let nodesChanged = canvasDataList.length !== this.currentDataList.length;
- if ( ! nodesChanged ) {
- for ( let i = 0; i < canvasDataList.length; i ++ ) {
- if ( canvasDataList[ i ].id !== this.currentDataList[ i ].id ) {
- nodesChanged = true;
- break;
- }
- }
- }
- if ( nodesChanged ) {
- const currentSelectedValue = this.select.value;
- // Clear options except the first one ('list')
- while ( this.select.options.length > 1 ) {
- this.select.remove( 1 );
- }
- // Add options for each node in canvasDataList
- for ( const canvasData of canvasDataList ) {
- const option = document.createElement( 'option' );
- option.value = canvasData.id;
- option.textContent = canvasData.path ? `${ canvasData.path } / ${ canvasData.name }` : canvasData.name;
- this.select.appendChild( option );
- }
- // Try to restore from saved view first on initial load
- let restored = false;
- if ( this.pendingRestoreView ) {
- const savedView = getItem( 'viewerLastView' );
- if ( savedView !== 'list' ) {
- for ( let i = 0; i < this.select.options.length; i ++ ) {
- if ( this.select.options[ i ].textContent === savedView ) {
- this.select.selectedIndex = i;
- const nodeId = this.select.options[ i ].value;
- this.showNodeView( nodeId );
- restored = true;
- this.pendingRestoreView = false;
- break;
- }
- }
- } else {
- this.pendingRestoreView = false;
- }
- }
- if ( ! restored ) {
- // Restore selection if still valid
- let hasSelectedValue = false;
- for ( let i = 0; i < this.select.options.length; i ++ ) {
- if ( this.select.options[ i ].value === currentSelectedValue ) {
- this.select.selectedIndex = i;
- hasSelectedValue = true;
- break;
- }
- }
- if ( ! hasSelectedValue ) {
- this.select.value = 'list';
- this.showListView();
- }
- }
- }
- // Real-time resize of active full-screen node canvas target
- if ( this.activeFullNodeId ) {
- const canvasData = canvasDataList.find( data => String( data.id ) === String( this.activeFullNodeId ) );
- if ( canvasData ) {
- const rect = this.fullViewerContainer.getBoundingClientRect();
- const contentWidth = rect.width || this.content.clientWidth;
- const contentHeight = rect.height || ( this.content.clientHeight - 38 );
- if ( canvasData.canvasTarget.domElement.width !== contentWidth || canvasData.canvasTarget.domElement.height !== contentHeight ) {
- canvasData.canvasTarget.setSize( contentWidth, contentHeight );
- renderer.backend.delete( canvasData.canvasTarget );
- }
- }
- }
- //
- const previousDataList = [ ...this.currentDataList ];
- // remove old
- for ( const canvasData of previousDataList ) {
- if ( this.itemLibrary.has( canvasData.id ) && canvasDataList.indexOf( canvasData ) === - 1 ) {
- const item = this.itemLibrary.get( canvasData.id );
- const parent = item.parent;
- parent.remove( item );
- if ( this.folderLibrary.has( parent.data[ 0 ] ) && parent.children.length === 0 ) {
- parent.parent.remove( parent );
- this.folderLibrary.delete( parent.data[ 0 ] );
- }
- this.itemLibrary.delete( canvasData.id );
- }
- }
- //
- const indexes = {};
- for ( const canvasData of canvasDataList ) {
- const item = this.addNodeItem( canvasData );
- const previousCanvasTarget = renderer.getCanvasTarget();
- const path = canvasData.path;
- if ( path ) {
- const folder = this.getFolder( path );
- if ( indexes[ path ] === undefined ) {
- indexes[ path ] = 0;
- }
- if ( folder.parent === null || item.parent !== folder || folder.children.indexOf( item ) !== indexes[ path ] ) {
- folder.add( item );
- }
- indexes[ path ] ++;
- } else {
- if ( ! item.parent ) {
- this.nodes.add( item );
- }
- }
- const rttNodes = [];
- const mainSize = previousCanvasTarget.getDrawingBufferSize( _size );
- canvasData.node.traverse( ( child ) => {
- if ( child.isRTTNode && child.autoResize === true ) {
- const oldWidth = child.width;
- const oldHeight = child.height;
- child.width = mainSize.width;
- child.height = mainSize.height;
- child.setSize( mainSize.width, mainSize.height );
- rttNodes.push( {
- node: child,
- oldWidth,
- oldHeight
- } );
- }
- } );
- const state = RendererUtils.resetRendererState( renderer );
- renderer.toneMapping = NoToneMapping;
- renderer.outputColorSpace = LinearSRGBColorSpace;
- renderer.setCanvasTarget( canvasData.canvasTarget );
- if ( canvasData.canvasAspect ) {
- canvasData.canvasAspect.value = canvasData.canvasTarget.domElement.width / canvasData.canvasTarget.domElement.height;
- }
- canvasData.quad.render( renderer );
- renderer.setCanvasTarget( previousCanvasTarget );
- RendererUtils.restoreRendererState( renderer, state );
- for ( const rtt of rttNodes ) {
- rtt.node.width = rtt.oldWidth;
- rtt.node.height = rtt.oldHeight;
- }
- }
- this.currentDataList = canvasDataList;
- }
- setActive( isActive ) {
- super.setActive( isActive );
- }
- saveLastView() {
- const selectedValue = this.select.value;
- if ( selectedValue === 'list' ) {
- setItem( 'viewerLastView', 'list' );
- } else {
- const selectedOption = this.select.options[ this.select.selectedIndex ];
- if ( selectedOption ) {
- setItem( 'viewerLastView', selectedOption.textContent );
- }
- }
- }
- }
- export { Viewer };
|