3D-Demo/jsm/node-editor/NodeEditor.js

779 lines
14 KiB
JavaScript
Raw Normal View History

2022-10-14 16:50:42 +08:00
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',
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',
nodeClass: BlendEditor
},
{
name: 'Normal Map',
icon: 'chart-line',
nodeClass: NormalMapEditor
}
]
},
{
name: 'Math',
icon: 'calculator',
children: [
{
name: 'Operator',
icon: 'math-symbols',
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 / ...',
nodeClass: TrigonometryEditor
},
{
name: 'Angle',
icon: 'angle',
tip: '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.nodesContext = null;
this.examplesContext = null;
this._initUpload();
this._initTips();
this._initMenu();
this._initSearch();
this._initNodesContext();
this._initExamplesContext();
}
centralizeNode( node ) {
const canvas = this.canvas;
const canvasRect = canvas.rect;
const nodeRect = node.dom.getBoundingClientRect();
const defaultOffsetX = nodeRect.width;
const defaultOffsetY = nodeRect.height;
node.setPosition(
( canvas.relativeX + ( canvasRect.width / 2 ) ) - defaultOffsetX,
( canvas.relativeY + ( canvasRect.height / 2 ) ) - defaultOffsetY
);
}
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;
}
newProject() {
this.canvas.clear();
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 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' );
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( examplesButton )
.add( menuButton )
.add( newButton )
.add( openButton )
.add( saveButton );
this.domElement.append( menu.dom );
this.menu = menu;
}
_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( basicContext, 'Matcap' );
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 );
} );
search.add( button );
}
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 );
} );
search.add( button );
}
} );
}
} );
search.onSubmit( () => {
if ( search.currentFiltered !== null ) {
search.currentFiltered.button.dispatchEvent( new Event( 'complete' ) );
}
} );
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 );
}
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;
}
}