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

857 lines
16 KiB

import { Styles, Canvas, CircleMenu, ButtonInput, ContextMenu, Tips, Search, Loader } from '../libs/flow.module.js';
import { BasicMaterialEditor } from './materials/BasicMaterialEditor.js';
import { StandardMaterialEditor } from './materials/StandardMaterialEditor.js';
import { PointsMaterialEditor } from './materials/PointsMaterialEditor.js';
import { OperatorEditor } from './math/OperatorEditor.js';
import { NormalizeEditor } from './math/NormalizeEditor.js';
import { InvertEditor } from './math/InvertEditor.js';
import { LimiterEditor } from './math/LimiterEditor.js';
import { DotEditor } from './math/DotEditor.js';
import { PowerEditor } from './math/PowerEditor.js';
import { AngleEditor } from './math/AngleEditor.js';
import { TrigonometryEditor } from './math/TrigonometryEditor.js';
import { FloatEditor } from './inputs/FloatEditor.js';
import { Vector2Editor } from './inputs/Vector2Editor.js';
import { Vector3Editor } from './inputs/Vector3Editor.js';
import { Vector4Editor } from './inputs/Vector4Editor.js';
import { SliderEditor } from './inputs/SliderEditor.js';
import { ColorEditor } from './inputs/ColorEditor.js';
import { TextureEditor } from './inputs/TextureEditor.js';
import { BlendEditor } from './display/BlendEditor.js';
import { NormalMapEditor } from './display/NormalMapEditor.js';
import { UVEditor } from './accessors/UVEditor.js';
import { MatcapUVEditor } from './accessors/MatcapUVEditor.js';
import { PositionEditor } from './accessors/PositionEditor.js';
import { NormalEditor } from './accessors/NormalEditor.js';
import { PreviewEditor } from './utils/PreviewEditor.js';
import { TimerEditor } from './utils/TimerEditor.js';
import { OscillatorEditor } from './utils/OscillatorEditor.js';
import { SplitEditor } from './utils/SplitEditor.js';
import { JoinEditor } from './utils/JoinEditor.js';
import { CheckerEditor } from './procedural/CheckerEditor.js';
import { PointsEditor } from './scene/PointsEditor.js';
import { MeshEditor } from './scene/MeshEditor.js';
import { FileEditor } from './core/FileEditor.js';
import { FileURLEditor } from './core/FileURLEditor.js';
import { EventDispatcher } from 'three';
Styles.icons.unlink = 'ti ti-unlink';
export const NodeList = [
{
name: 'Inputs',
icon: 'forms',
children: [
{
name: 'Slider',
icon: 'adjustments-horizontal',
tags: 'number',
nodeClass: SliderEditor
},
{
name: 'Float',
icon: 'box-multiple-1',
nodeClass: FloatEditor
},
{
name: 'Vector 2',
icon: 'box-multiple-2',
nodeClass: Vector2Editor
},
{
name: 'Vector 3',
icon: 'box-multiple-3',
nodeClass: Vector3Editor
},
{
name: 'Vector 4',
icon: 'box-multiple-4',
nodeClass: Vector4Editor
},
{
name: 'Color',
icon: 'palette',
nodeClass: ColorEditor
},
{
name: 'Texture',
icon: 'photo',
nodeClass: TextureEditor
},
{
name: 'File URL',
icon: 'cloud-download',
nodeClass: FileURLEditor
}
]
},
{
name: 'Accessors',
icon: 'vector-triangle',
children: [
{
name: 'UV',
icon: 'details',
nodeClass: UVEditor
},
{
name: 'Position',
icon: 'hierarchy',
nodeClass: PositionEditor
},
{
name: 'Normal',
icon: 'fold-up',
nodeClass: NormalEditor
},
{
name: 'Matcap UV',
icon: 'circle',
nodeClass: MatcapUVEditor
}
]
},
{
name: 'Display',
icon: 'brightness',
children: [
{
name: 'Blend',
icon: 'layers-subtract',
tags: 'mix',
nodeClass: BlendEditor
},
{
name: 'Normal Map',
icon: 'chart-line',
nodeClass: NormalMapEditor
}
]
},
{
name: 'Math',
icon: 'calculator',
children: [
{
name: 'Operator',
icon: 'math-symbols',
tags: 'addition, subtration, multiplication, division',
nodeClass: OperatorEditor
},
{
name: 'Invert',
icon: 'flip-vertical',
tip: 'Negate',
nodeClass: InvertEditor
},
{
name: 'Limiter',
icon: 'arrow-bar-to-up',
tip: 'Min / Max',
nodeClass: LimiterEditor
},
{
name: 'Dot Product',
icon: 'arrows-up-left',
nodeClass: DotEditor
},
{
name: 'Power',
icon: 'arrow-up-right',
nodeClass: PowerEditor
},
{
name: 'Trigonometry',
icon: 'wave-sine',
tip: 'Sin / Cos / Tan / ...',
tags: 'sin, cos, tan, asin, acos, atan, sine, cosine, tangent, arcsine, arccosine, arctangent',
nodeClass: TrigonometryEditor
},
{
name: 'Angle',
icon: 'angle',
tip: 'Degress / Radians',
tags: 'degress, radians',
nodeClass: AngleEditor
},
{
name: 'Normalize',
icon: 'fold',
nodeClass: NormalizeEditor
}
]
},
{
name: 'Procedural',
icon: 'infinity',
children: [
{
name: 'Checker',
icon: 'border-outer',
nodeClass: CheckerEditor
}
]
},
{
name: 'Utils',
icon: 'apps',
children: [
{
name: 'Preview',
icon: 'square-check',
nodeClass: PreviewEditor
},
{
name: 'Timer',
icon: 'clock',
nodeClass: TimerEditor
},
{
name: 'Oscillator',
icon: 'wave-sine',
nodeClass: OscillatorEditor
},
{
name: 'Split',
icon: 'arrows-split-2',
nodeClass: SplitEditor
},
{
name: 'Join',
icon: 'arrows-join-2',
nodeClass: JoinEditor
}
]
},
/*{
name: 'Scene',
icon: '3d-cube-sphere',
children: [
{
name: 'Mesh',
icon: '3d-cube-sphere',
nodeClass: MeshEditor
}
]
},*/
{
name: 'Material',
icon: 'circles',
children: [
{
name: 'Basic Material',
icon: 'circle',
nodeClass: BasicMaterialEditor
},
{
name: 'Standard Material',
icon: 'circle',
nodeClass: StandardMaterialEditor
},
{
name: 'Points Material',
icon: 'circle-dotted',
nodeClass: PointsMaterialEditor
}
]
}
];
export const ClassLib = {
BasicMaterialEditor,
StandardMaterialEditor,
PointsMaterialEditor,
PointsEditor,
MeshEditor,
OperatorEditor,
NormalizeEditor,
InvertEditor,
LimiterEditor,
DotEditor,
PowerEditor,
AngleEditor,
TrigonometryEditor,
FloatEditor,
Vector2Editor,
Vector3Editor,
Vector4Editor,
SliderEditor,
ColorEditor,
TextureEditor,
BlendEditor,
NormalMapEditor,
UVEditor,
MatcapUVEditor,
PositionEditor,
NormalEditor,
TimerEditor,
OscillatorEditor,
SplitEditor,
JoinEditor,
CheckerEditor,
FileURLEditor
};
export class NodeEditor extends EventDispatcher {
constructor( scene = null ) {
super();
const domElement = document.createElement( 'flow' );
const canvas = new Canvas();
domElement.append( canvas.dom );
this.scene = scene;
this.canvas = canvas;
this.domElement = domElement;
this._preview = false;
this.search = null;
this.menu = null;
this.previewMenu = null;
this.nodesContext = null;
this.examplesContext = null;
this._initUpload();
this._initTips();
this._initMenu();
this._initSearch();
this._initNodesContext();
this._initExamplesContext();
}
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.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();
}
this._preview = value;
}
get preview() {
return this._preview;
}
newProject() {
const canvas = this.canvas;
canvas.clear();
canvas.scrollLeft = 0;
canvas.scrollTop = 0;
canvas.zoom = 1;
this.dispatchEvent( { type: 'new' } );
}
loadJSON( json ) {
const canvas = this.canvas;
canvas.clear();
canvas.deserialize( json );
for ( const node of canvas.nodes ) {
this.add( node );
}
this.dispatchEvent( { type: 'load' } );
}
_initUpload() {
const canvas = this.canvas;
canvas.onDrop( () => {
for ( const item of canvas.droppedItems ) {
if ( /^image\//.test( item.type ) === true ) {
const { relativeClientX, relativeClientY } = canvas;
const file = item.getAsFile();
const fileEditor = new FileEditor( file );
fileEditor.setPosition(
relativeClientX - ( fileEditor.getWidth() / 2 ),
relativeClientY - 20
);
this.add( fileEditor );
}
}
} );
}
_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-3d-cube-sphere' ).setToolTip( 'Preview' );
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 );
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( () => {
const json = JSON.stringify( this.canvas.toJSON() );
const a = document.createElement( 'a' );
const file = new Blob( [ json ], { type: 'text/plain' } );
a.href = URL.createObjectURL( file );
a.download = 'node_editor.json';
a.click();
} );
menu.add( previewButton )
.add( newButton )
.add( examplesButton )
.add( openButton )
.add( saveButton )
.add( menuButton );
previewMenu.add( editorButton );
this.domElement.append( 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();
const loader = new Loader( Loader.OBJECTS );
const json = await loader.load( `./jsm/node-editor/examples/${filename}.json`, ClassLib );
this.loadJSON( json );
};
const addExample = ( context, name, filename = null ) => {
filename = filename || name.replaceAll( ' ', '-' ).toLowerCase();
context.add( new ButtonInput( name )
.setIcon( 'ti ti-file-symlink' )
.onClick( onClickExample )
.setExtra( filename )
);
};
//**************//
// EXAMPLES
//**************//
const basicContext = new ContextMenu();
const advancedContext = new ContextMenu();
addExample( basicContext, 'Animate UV' );
addExample( basicContext, 'Fake top light' );
addExample( basicContext, 'Oscillator color' );
addExample( advancedContext, 'Rim' );
//**************//
// MAIN
//**************//
context.add( new ButtonInput( 'Basic' ), basicContext );
context.add( new ButtonInput( 'Advanced' ), advancedContext );
this.examplesContext = context;
}
_initSearch() {
const traverseNodeEditors = ( item ) => {
if ( item.nodeClass ) {
const button = new ButtonInput( item.name );
button.setIcon( `ti ti-${item.icon}` );
button.addEventListener( 'complete', () => {
const node = new item.nodeClass();
this.add( node );
this.centralizeNode( node );
this.canvas.select( node );
} );
search.add( button );
if ( item.tags !== undefined ) {
search.setTag( button, item.tags );
}
}
if ( item.children ) {
for ( const subItem of item.children ) {
traverseNodeEditors( subItem );
}
}
};
const search = new Search();
search.forceAutoComplete = true;
search.onFilter( () => {
search.clear();
for ( const item of NodeList ) {
traverseNodeEditors( item );
}
const object3d = this.scene;
if ( object3d !== null ) {
object3d.traverse( ( obj3d ) => {
if ( obj3d.isMesh === true || obj3d.isPoints === true ) {
let prefix = null;
let icon = null;
let editorClass = null;
if ( obj3d.isMesh === true ) {
prefix = 'Mesh';
icon = 'ti ti-3d-cube-sphere';
editorClass = MeshEditor;
} else if ( obj3d.isPoints === true ) {
prefix = 'Points';
icon = 'ti ti-border-none';
editorClass = PointsEditor;
}
const button = new ButtonInput( `${prefix} - ${obj3d.name}` );
button.setIcon( icon );
button.addEventListener( 'complete', () => {
for ( const node of this.canvas.nodes ) {
if ( node.value === obj3d ) {
// prevent duplicated node
this.canvas.select( node );
return;
}
}
const node = new editorClass( obj3d );
this.add( node );
this.centralizeNode( node );
this.canvas.select( node );
} );
search.add( button );
}
} );
}
} );
search.onSubmit( () => {
if ( search.currentFiltered !== null ) {
search.currentFiltered.button.dispatchEvent( new Event( 'complete' ) );
}
} );
this.search = search;
this.domElement.append( search.dom );
}
_initNodesContext() {
const context = new ContextMenu( this.domElement );
let isContext = false;
const contextPosition = {};
const add = ( node ) => {
if ( isContext ) {
node.setPosition(
Math.round( contextPosition.x ),
Math.round( contextPosition.y )
);
} else {
this.centralizeNode( node );
this.canvas.select( node );
}
context.hide();
this.add( 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 );
} );
//**************//
// INPUTS
//**************//
const createButtonMenu = ( item ) => {
const button = new ButtonInput( item.name );
button.setIcon( `ti ti-${item.icon}` );
let context = null;
if ( item.nodeClass ) {
button.onClick( () => add( new item.nodeClass() ) );
}
if ( item.tip ) {
button.setToolTip( item.tip );
}
if ( item.children ) {
context = new ContextMenu();
for ( const subItem of item.children ) {
const buttonMenu = createButtonMenu( subItem );
context.add( buttonMenu.button, buttonMenu.context );
}
}
return { button, context };
};
for ( const item of NodeList ) {
const buttonMenu = createButtonMenu( item );
context.add( buttonMenu.button, buttonMenu.context );
}
this.nodesContext = context;
}
}