import { BaseNodeEditor } from '../BaseNodeEditor.js'; import { CodeEditorElement } from '../elements/CodeEditorElement.js'; import { disposeScene, createElementFromJSON, isGPUNode, onValidType } from '../NodeEditorUtils.js'; import { global, scriptable, js, scriptableValue } from 'three/nodes'; import { getColorFromType, setInputAestheticsFromType, setOutputAestheticsFromType } from '../DataTypeLib.js'; const defaultTitle = 'Scriptable'; const defaultWidth = 500; export class ScriptableEditor extends BaseNodeEditor { constructor( source = null, enableEditor = true ) { let codeNode = null; let scriptableNode = null; if ( source && source.isCodeNode ) { codeNode = source; } else { codeNode = js( source || '' ); } scriptableNode = scriptable( codeNode ); super( defaultTitle, scriptableNode, defaultWidth ); this.scriptableNode = scriptableNode; this.editorCodeNode = codeNode; this.editorOutput = null; this.editorOutputAdded = null; this.layout = null; this.editorElement = null; this.layoutJSON = ''; this.initCacheKey = ''; this.initId = 0; this.waitToLayoutJSON = null; this.hasInternalEditor = false; this._updating = false; this.onValidElement = () => {}; if ( enableEditor ) { this.title.setSerializable( true ); this._initExternalConnection(); this._toInternal(); } const defaultOutput = this.scriptableNode.getDefaultOutput(); defaultOutput.events.addEventListener( 'refresh', () => { this.update(); } ); this.update(); } getColor() { const color = getColorFromType( this.layout ? this.layout.outputType : null ); return color ? color + 'BB' : null; } hasJSON() { return true; } exportJSON() { return this.scriptableNode.toJSON(); } setSource( source ) { this.editorCodeNode.code = source; this.update(); return this; } update( force = false ) { if ( this._updating === true ) return; this._updating = true; this.scriptableNode.codeNode = this.codeNode; this.scriptableNode.needsUpdate = true; let layout = null; let scriptableValueOutput = null; try { const object = this.scriptableNode.getObject(); layout = this.scriptableNode.getLayout(); this.updateLayout( layout, force ); scriptableValueOutput = this.scriptableNode.getDefaultOutput(); const initCacheKey = typeof object.init === 'function' ? object.init.toString() : ''; if ( initCacheKey !== this.initCacheKey ) { this.initCacheKey = initCacheKey; const initId = ++ this.initId; this.scriptableNode.callAsync( 'init' ).then( () => { if ( initId === this.initId ) { this.update(); if ( this.editor ) this.editor.tips.message( 'ScriptEditor: Initialized.' ); } } ); } } catch ( e ) { console.error( e ); if ( this.editor ) this.editor.tips.error( e.message ); } const editorOutput = scriptableValueOutput ? scriptableValueOutput.value : null; this.value = isGPUNode( editorOutput ) ? this.scriptableNode : scriptableValueOutput; this.layout = layout; this.editorOutput = editorOutput; this.updateOutputInEditor(); this.updateOutputConnection(); this.invalidate(); this._updating = false; } updateOutputConnection() { const layout = this.layout; if ( layout ) { const outputType = layout.outputType; setOutputAestheticsFromType( this.title, outputType ); } else { this.title.setOutput( 0 ); } } updateOutputInEditor() { const { editor, editorOutput, editorOutputAdded } = this; if ( editor && editorOutput === editorOutputAdded ) return; const scene = global.get( 'scene' ); const composer = global.get( 'composer' ); if ( editor ) { if ( editorOutputAdded && editorOutputAdded.isObject3D === true ) { editorOutputAdded.removeFromParent(); disposeScene( editorOutputAdded ); } else if ( composer && editorOutputAdded && editorOutputAdded.isPass === true ) { composer.removePass( editorOutputAdded ); } if ( editorOutput && editorOutput.isObject3D === true ) { scene.add( editorOutput ); } else if ( composer && editorOutput && editorOutput.isPass === true ) { composer.addPass( editorOutput ); } this.editorOutputAdded = editorOutput; } else { if ( editorOutputAdded && editorOutputAdded.isObject3D === true ) { editorOutputAdded.removeFromParent(); disposeScene( editorOutputAdded ); } else if ( composer && editorOutputAdded && editorOutputAdded.isPass === true ) { composer.removePass( editorOutputAdded ); } this.editorOutputAdded = null; } } setEditor( editor ) { super.setEditor( editor ); this.updateOutputInEditor(); } clearParameters() { this.layoutJSON = ''; this.scriptableNode.clearParameters(); for ( const element of this.elements.concat() ) { if ( element !== this.editorElement && element !== this.title ) { this.remove( element ); } } } addElementFromJSON( json ) { const { id, element, inputNode, outputType } = createElementFromJSON( json ); this.add( element ); this.scriptableNode.setParameter( id, inputNode ); if ( outputType ) { element.setObjectCallback( () => { return this.scriptableNode.getOutput( id ); } ); } // const onUpdate = () => { const value = element.value; const paramValue = value && value.isScriptableValueNode ? value : scriptableValue( value ); this.scriptableNode.setParameter( id, paramValue ); this.update(); }; element.addEventListener( 'changeInput', onUpdate ); element.onConnect( onUpdate, true ); //element.onConnect( () => this.getScriptable().call( 'onDeepChange' ), true ); return element; } updateLayout( layout = null, force = false ) { const needsUpdateWidth = this.hasExternalEditor || this.editorElement === null; if ( this.waitToLayoutJSON !== null ) { if ( this.waitToLayoutJSON === JSON.stringify( layout || '{}' ) ) { this.waitToLayoutJSON = null; if ( needsUpdateWidth ) this.setWidth( layout.width ); } else { return; } } if ( layout ) { const layoutCacheKey = JSON.stringify( layout ); if ( this.layoutJSON !== layoutCacheKey || force === true ) { this.clearParameters(); if ( layout.name ) { this.setName( layout.name ); } if ( layout.icon ) { this.setIcon( layout.icon ); } if ( needsUpdateWidth ) { if ( layout.width !== undefined ) { this.setWidth( layout.width ); } else { this.setWidth( defaultWidth ); } } if ( layout.elements ) { for ( const element of layout.elements ) { this.addElementFromJSON( element ); } if ( this.editorElement ) { this.remove( this.editorElement ); this.add( this.editorElement ); } } this.layoutJSON = layoutCacheKey; } } else { this.setName( defaultTitle ); this.setIcon( null ); this.setWidth( defaultWidth ); this.clearParameters(); } this.updateOutputConnection(); } get hasExternalEditor() { return this.title.getLinkedObject() !== null; } get codeNode() { return this.hasExternalEditor ? this.title.getLinkedObject() : this.editorCodeNode; } _initExternalConnection() { setInputAestheticsFromType(this.title, 'CodeNode' ).onValid( onValidType( 'CodeNode' ) ).onConnect( () => { this.hasExternalEditor ? this._toExternal() : this._toInternal(); this.update(); }, true ); } _toInternal() { if ( this.hasInternalEditor === true ) return; if ( this.editorElement === null ) { this.editorElement = new CodeEditorElement( this.editorCodeNode.code ); this.editorElement.addEventListener( 'change', () => { this.setSource( this.editorElement.source ); this.editorElement.focus(); } ); this.add( this.editorElement ); } this.setResizable( true ); this.editorElement.setVisible( true ); this.hasInternalEditor = true; this.update( /*true*/ ); } _toExternal() { if ( this.hasInternalEditor === false ) return; this.editorElement.setVisible( false ); this.setResizable( false ); this.hasInternalEditor = false; this.update( /*true*/ ); } serialize( data ) { super.serialize( data ); data.layoutJSON = this.layoutJSON; } deserialize( data ) { this.updateLayout( JSON.parse( data.layoutJSON || '{}' ), true ); this.waitToLayoutJSON = data.layoutJSON; super.deserialize( data ); } }