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.

797 lines
14 KiB
JavaScript

import * as THREE from 'three';
import * as TSL from 'three/tsl';
import { Canvas, CircleMenu, ButtonInput, StringInput, ContextMenu, Tips, Search, Loader, Node, TreeViewNode, TreeViewInput, Element } from 'flow';
import { FileEditor } from './editors/FileEditor.js';
import { exportJSON } from './NodeEditorUtils.js';
import { init, ClassLib, getNodeEditorClass, getNodeList } from './NodeEditorLib.js';
import { SplitscreenManager } from './SplitscreenManager.js';
init();
Element.icons.unlink = 'ti ti-unlink';
export class NodeEditor extends THREE.EventDispatcher {
constructor( scene = null, renderer = null, composer = null ) {
super();
const domElement = document.createElement( 'flow' );
const canvas = new Canvas();
domElement.append( canvas.dom );
this.scene = scene;
this.renderer = renderer;
const { ScriptableNodeResources } = TSL;
ScriptableNodeResources.set( 'THREE', THREE );
ScriptableNodeResources.set( 'TSL', TSL );
ScriptableNodeResources.set( 'scene', scene );
ScriptableNodeResources.set( 'renderer', renderer );
ScriptableNodeResources.set( 'composer', composer );
this.nodeClasses = [];
this.canvas = canvas;
this.domElement = domElement;
this._preview = false;
this._splitscreen = false;
this.search = null;
this.menu = null;
this.previewMenu = null;
this.nodesContext = null;
this.examplesContext = null;
this._initSplitview();
this._initUpload();
this._initTips();
this._initMenu();
this._initSearch();
this._initNodesContext();
this._initExamplesContext();
this._initShortcuts();
this._initParams();
}
setSize( width, height ) {
this.canvas.setSize( width, height );
return this;
}
centralizeNode( node ) {
const canvas = this.canvas;
const nodeRect = node.dom.getBoundingClientRect();
node.setPosition(
( ( canvas.width / 2 ) - canvas.scrollLeft ) - nodeRect.width,
( ( canvas.height / 2 ) - canvas.scrollTop ) - nodeRect.height
);
return this;
}
add( node ) {
const onRemove = () => {
node.removeEventListener( 'remove', onRemove );
node.setEditor( null );
};
node.setEditor( this );
node.addEventListener( 'remove', onRemove );
this.canvas.add( node );
this.dispatchEvent( { type: 'add', node } );
return this;
}
get nodes() {
return this.canvas.nodes;
}
set preview( value ) {
if ( this._preview === value ) return;
if ( value ) {
this._wasSplitscreen = this.splitscreen;
this.splitscreen = false;
this.menu.dom.remove();
this.canvas.dom.remove();
this.search.dom.remove();
this.domElement.append( this.previewMenu.dom );
} else {
this.canvas.focusSelected = false;
this.domElement.append( this.menu.dom );
this.domElement.append( this.canvas.dom );
this.domElement.append( this.search.dom );
this.previewMenu.dom.remove();
if ( this._wasSplitscreen == true ) {
this.splitscreen = true;
}
}
this._preview = value;
}
get preview() {
return this._preview;
}
set splitscreen( value ) {
if ( this._splitscreen === value ) return;
this.splitview.setSplitview( value );
this._splitscreen = value;
}
get splitscreen() {
return this._splitscreen;
}
newProject() {
const canvas = this.canvas;
canvas.clear();
canvas.scrollLeft = 0;
canvas.scrollTop = 0;
canvas.zoom = 1;
this.dispatchEvent( { type: 'new' } );
}
async loadURL( url ) {
const loader = new Loader( Loader.OBJECTS );
const json = await loader.load( url, ClassLib );
this.loadJSON( json );
}
loadJSON( json ) {
const canvas = this.canvas;
canvas.clear();
canvas.deserialize( json );
for ( const node of canvas.nodes ) {
this.add( node );
}
this.dispatchEvent( { type: 'load' } );
}
_initSplitview() {
this.splitview = new SplitscreenManager( this );
}
_initUpload() {
const canvas = this.canvas;
canvas.onDrop( () => {
for ( const item of canvas.droppedItems ) {
const { relativeClientX, relativeClientY } = canvas;
const file = item.getAsFile();
const reader = new FileReader();
reader.onload = () => {
const fileEditor = new FileEditor( reader.result, file.name );
fileEditor.setPosition(
relativeClientX - ( fileEditor.getWidth() / 2 ),
relativeClientY - 20
);
this.add( fileEditor );
};
reader.readAsArrayBuffer( file );
}
} );
}
_initTips() {
this.tips = new Tips();
this.domElement.append( this.tips.dom );
}
_initMenu() {
const menu = new CircleMenu();
const previewMenu = new CircleMenu();
menu.setAlign( 'top left' );
previewMenu.setAlign( 'top left' );
const previewButton = new ButtonInput().setIcon( 'ti ti-brand-threejs' ).setToolTip( 'Preview' );
const splitscreenButton = new ButtonInput().setIcon( 'ti ti-layout-sidebar-right-expand' ).setToolTip( 'Splitscreen' );
const menuButton = new ButtonInput().setIcon( 'ti ti-apps' ).setToolTip( 'Add' );
const examplesButton = new ButtonInput().setIcon( 'ti ti-file-symlink' ).setToolTip( 'Examples' );
const newButton = new ButtonInput().setIcon( 'ti ti-file' ).setToolTip( 'New' );
const openButton = new ButtonInput().setIcon( 'ti ti-upload' ).setToolTip( 'Open' );
const saveButton = new ButtonInput().setIcon( 'ti ti-download' ).setToolTip( 'Save' );
const editorButton = new ButtonInput().setIcon( 'ti ti-subtask' ).setToolTip( 'Editor' );
previewButton.onClick( () => this.preview = true );
editorButton.onClick( () => this.preview = false );
splitscreenButton.onClick( () => {
this.splitscreen = ! this.splitscreen;
splitscreenButton.setIcon( this.splitscreen ? 'ti ti-layout-sidebar-right-collapse' : 'ti ti-layout-sidebar-right-expand' );
} );
menuButton.onClick( () => this.nodesContext.open() );
examplesButton.onClick( () => this.examplesContext.open() );
newButton.onClick( () => {
if ( confirm( 'Are you sure?' ) === true ) {
this.newProject();
}
} );
openButton.onClick( () => {
const input = document.createElement( 'input' );
input.type = 'file';
input.onchange = e => {
const file = e.target.files[ 0 ];
const reader = new FileReader();
reader.readAsText( file, 'UTF-8' );
reader.onload = readerEvent => {
const loader = new Loader( Loader.OBJECTS );
const json = loader.parse( JSON.parse( readerEvent.target.result ), ClassLib );
this.loadJSON( json );
};
};
input.click();
} );
saveButton.onClick( () => {
exportJSON( this.canvas.toJSON(), 'node_editor' );
} );
menu.add( previewButton )
.add( splitscreenButton )
.add( newButton )
.add( examplesButton )
.add( openButton )
.add( saveButton )
.add( menuButton );
previewMenu.add( editorButton );
this.domElement.appendChild( menu.dom );
this.menu = menu;
this.previewMenu = previewMenu;
}
_initExamplesContext() {
const context = new ContextMenu();
//**************//
// MAIN
//**************//
const onClickExample = async ( button ) => {
this.examplesContext.hide();
const filename = button.getExtra();
this.loadURL( `./examples/${filename}.json` );
};
const addExamples = ( category, names ) => {
const subContext = new ContextMenu();
for ( const name of names ) {
const filename = name.replaceAll( ' ', '-' ).toLowerCase();
subContext.add( new ButtonInput( name )
.setIcon( 'ti ti-file-symlink' )
.onClick( onClickExample )
.setExtra( category.toLowerCase() + '/' + filename )
);
}
context.add( new ButtonInput( category ), subContext );
return subContext;
};
//**************//
// EXAMPLES
//**************//
addExamples( 'Basic', [
'Teapot',
'Matcap',
'Fresnel',
'Particles'
] );
this.examplesContext = context;
}
_initShortcuts() {
document.addEventListener( 'keydown', ( e ) => {
if ( e.target === document.body ) {
const key = e.key;
if ( key === 'Tab' ) {
this.search.inputDOM.focus();
e.preventDefault();
e.stopImmediatePropagation();
} else if ( key === ' ' ) {
this.preview = ! this.preview;
} else if ( key === 'Delete' ) {
if ( this.canvas.selected ) this.canvas.selected.dispose();
} else if ( key === 'Escape' ) {
this.canvas.select( null );
}
}
} );
}
_initParams() {
const urlParams = new URLSearchParams( window.location.search );
const example = urlParams.get( 'example' ) || 'basic/teapot';
this.loadURL( `./examples/${example}.json` );
}
addClass( nodeData ) {
this.removeClass( nodeData );
this.nodeClasses.push( nodeData );
ClassLib[ nodeData.name ] = nodeData.nodeClass;
return this;
}
removeClass( nodeData ) {
const index = this.nodeClasses.indexOf( nodeData );
if ( index !== - 1 ) {
this.nodeClasses.splice( index, 1 );
delete ClassLib[ nodeData.name ];
}
return this;
}
_initSearch() {
const traverseNodeEditors = ( item ) => {
if ( item.children ) {
for ( const subItem of item.children ) {
traverseNodeEditors( subItem );
}
} else {
const button = new ButtonInput( item.name );
button.setIcon( `ti ti-${item.icon}` );
button.addEventListener( 'complete', async () => {
const nodeClass = await getNodeEditorClass( item );
const node = new nodeClass();
this.add( node );
this.centralizeNode( node );
this.canvas.select( node );
} );
search.add( button );
if ( item.tags !== undefined ) {
search.setTag( button, item.tags );
}
}
};
const search = new Search();
search.forceAutoComplete = true;
search.onFilter( async () => {
search.clear();
const nodeList = await getNodeList();
for ( const item of nodeList.nodes ) {
traverseNodeEditors( item );
}
for ( const item of this.nodeClasses ) {
traverseNodeEditors( item );
}
} );
search.onSubmit( () => {
if ( search.currentFiltered !== null ) {
search.currentFiltered.button.dispatchEvent( new Event( 'complete' ) );
}
} );
this.search = search;
this.domElement.append( search.dom );
}
async _initNodesContext() {
const context = new ContextMenu( this.canvas.canvas ).setWidth( 300 );
let isContext = false;
const contextPosition = {};
const add = ( node ) => {
context.hide();
this.add( node );
if ( isContext ) {
node.setPosition(
Math.round( contextPosition.x ),
Math.round( contextPosition.y )
);
} else {
this.centralizeNode( node );
}
this.canvas.select( node );
isContext = false;
};
context.onContext( () => {
isContext = true;
const { relativeClientX, relativeClientY } = this.canvas;
contextPosition.x = Math.round( relativeClientX );
contextPosition.y = Math.round( relativeClientY );
} );
context.addEventListener( 'show', () => {
reset();
focus();
} );
//**************//
// INPUTS
//**************//
const nodeButtons = [];
let nodeButtonsVisible = [];
let nodeButtonsIndex = - 1;
const focus = () => requestAnimationFrame( () => search.inputDOM.focus() );
const reset = () => {
search.setValue( '', false );
for ( const button of nodeButtons ) {
button.setOpened( false ).setVisible( true ).setSelected( false );
}
};
const node = new Node();
context.add( node );
const search = new StringInput().setPlaceHolder( 'Search...' ).setIcon( 'ti ti-list-search' );
search.inputDOM.addEventListener( 'keydown', e => {
const key = e.key;
if ( key === 'ArrowDown' ) {
const previous = nodeButtonsVisible[ nodeButtonsIndex ];
if ( previous ) previous.setSelected( false );
const current = nodeButtonsVisible[ nodeButtonsIndex = ( nodeButtonsIndex + 1 ) % nodeButtonsVisible.length ];
if ( current ) current.setSelected( true );
e.preventDefault();
e.stopImmediatePropagation();
} else if ( key === 'ArrowUp' ) {
const previous = nodeButtonsVisible[ nodeButtonsIndex ];
if ( previous ) previous.setSelected( false );
const current = nodeButtonsVisible[ nodeButtonsIndex > 0 ? -- nodeButtonsIndex : ( nodeButtonsIndex = nodeButtonsVisible.length - 1 ) ];
if ( current ) current.setSelected( true );
e.preventDefault();
e.stopImmediatePropagation();
} else if ( key === 'Enter' ) {
if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {
nodeButtonsVisible[ nodeButtonsIndex ].dom.click();
} else {
context.hide();
}
e.preventDefault();
e.stopImmediatePropagation();
} else if ( key === 'Escape' ) {
context.hide();
}
} );
search.onChange( () => {
const value = search.getValue().toLowerCase();
if ( value.length === 0 ) return reset();
nodeButtonsVisible = [];
nodeButtonsIndex = 0;
for ( const button of nodeButtons ) {
const buttonLabel = button.getLabel().toLowerCase();
button.setVisible( false ).setSelected( false );
const visible = buttonLabel.indexOf( value ) !== - 1;
if ( visible && button.children.length === 0 ) {
nodeButtonsVisible.push( button );
}
}
for ( const button of nodeButtonsVisible ) {
let parent = button;
while ( parent !== null ) {
parent.setOpened( true ).setVisible( true );
parent = parent.parent;
}
}
if ( nodeButtonsVisible[ nodeButtonsIndex ] !== undefined ) {
nodeButtonsVisible[ nodeButtonsIndex ].setSelected( true );
}
} );
const treeView = new TreeViewInput();
node.add( new Element().setHeight( 30 ).add( search ) );
node.add( new Element().setHeight( 200 ).add( treeView ) );
const addNodeEditorElement = ( nodeData ) => {
const button = new TreeViewNode( nodeData.name );
button.setIcon( `ti ti-${nodeData.icon}` );
if ( nodeData.children === undefined ) {
button.isNodeClass = true;
button.onClick( async () => {
const nodeClass = await getNodeEditorClass( nodeData );
add( new nodeClass() );
} );
}
if ( nodeData.tip ) {
//button.setToolTip( item.tip );
}
nodeButtons.push( button );
if ( nodeData.children ) {
for ( const subItem of nodeData.children ) {
const subButton = addNodeEditorElement( subItem );
button.add( subButton );
}
}
return button;
};
//
const nodeList = await getNodeList();
for ( const node of nodeList.nodes ) {
const button = addNodeEditorElement( node );
treeView.add( button );
}
this.nodesContext = context;
}
}