You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

487 lines
8.4 KiB
JavaScript

import { BaseNodeEditor } from '../BaseNodeEditor.js';
import { CodeEditorElement } from '../elements/CodeEditorElement.js';
import { disposeScene, resetScene, createElementFromJSON, isGPUNode, onValidType } from '../NodeEditorUtils.js';
import { ScriptableNodeResources, scriptable, js, scriptableValue } from 'three/tsl';
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 = ScriptableNodeResources.get( 'scene' );
const composer = ScriptableNodeResources.get( 'composer' );
if ( editor ) {
if ( editorOutputAdded && editorOutputAdded.isObject3D === true ) {
editorOutputAdded.removeFromParent();
disposeScene( editorOutputAdded );
resetScene( scene );
} 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 );
resetScene( scene );
} 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 );
}
}