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
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;
|
|
|
|
}
|
|
|
|
}
|
|
|