lexgui 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,10 @@ function firstNonspaceIndex(str) {
29
29
  return str.search(/\S|$/);
30
30
  }
31
31
 
32
+ function deleteElement( el ) {
33
+ if( el ) el.remove();
34
+ }
35
+
32
36
  let ASYNC_ENABLED = true;
33
37
 
34
38
  function doAsync( fn, ms ) {
@@ -38,26 +42,68 @@ function doAsync( fn, ms ) {
38
42
  fn();
39
43
  }
40
44
 
45
+ class BoundingBox {
46
+
47
+ constructor( o, s )
48
+ {
49
+ this.origin = o ?? new LX.vec2( 0, 0 );
50
+ this.size = s ?? new LX.vec2( 0, 0 );
51
+ }
52
+
53
+ merge ( bb ) {
54
+
55
+ console.assert( bb.constructor == BoundingBox );
56
+
57
+ const min_0 = this.origin;
58
+ const max_0 = this.origin.add( this.size );
59
+
60
+ const min_1 = bb.origin;
61
+ const max_1 = bb.origin.add( bb.size );
62
+
63
+ const merge_min = new LX.vec2( Math.min( min_0.x, min_1.x ), Math.min( min_0.y, min_1.y ) );
64
+ const merge_max = new LX.vec2( Math.max( max_0.x, max_1.x ), Math.max( max_0.y, max_1.y ) );
65
+
66
+ this.origin = merge_min;
67
+ this.size = merge_max.sub( merge_min );
68
+ }
69
+
70
+ inside ( bb ) {
71
+
72
+ const min_0 = this.origin;
73
+ const max_0 = this.origin.add( this.size );
74
+
75
+ const min_1 = bb.origin;
76
+ const max_1 = bb.origin.add( bb.size );
77
+
78
+ return min_1.x >= min_0.x && max_1.x <= max_0.x
79
+ && min_1.y >= min_0.y && max_1.y <= max_0.y;
80
+ }
81
+ };
82
+
41
83
  /**
42
- * @class GraphCanvas
84
+ * @class GraphEditor
43
85
  */
44
86
 
45
- class GraphCanvas {
87
+ class GraphEditor {
46
88
 
47
89
  static __instances = [];
48
90
 
49
- static BACK_IMAGE_SRC = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAplJREFUeF7tm8FqwzAQRG2DDynEvaT//5fNKXFRCqZQkjLSyEzxC/Qmb5eZN9pYasfL5WMd+MQoMBZDxnEc5nl+2tS6rkP5mabpZeNlzf1+f6wrNZ99jrau6HC73f7UpazbDDmfFwsl1+vncDq9WWqVIkerhyGN6LiBwRAM0RRwE5hej4RofPxa7TYYQzBEU8BNYHo9EqLxsc+WVX7Lsrw3tvb9eDqB6f2RkEYM3QZjCIZoCrgJTK9HQjQ+9hnq5WSWw8U6Z9yJIyF1PmxPYUiYgBiCIZoCbmKOVu8xQ4rkr65wNUtY3aIAQ71FvQ5HRRiCIZoCh5whvBhqkPxc7QaGLaveiy7XDRiCIZoC7i0hvR4J0fjgtDedaHd/25s6d+p1UeliCF9768zo8UcdzJB6L/ja26hdFwHZshpdcQvorseWFWYwhmCIpoB7S0ivR0I0PnhTTyfa3R936o0JcT/OltWoaJeEcHRS7wqG1GvHm3qjdv9CQBLS6LJbQHc9hnqYwRiCIZoC7i0hvR4J0fjY5+ik/Bbu1OuccSeOhNT5sD2FIWECYgiGaAq4iTlaPWaIxts+37I47a13xZ1gElLvRZfDTwzBEE0B95aQXo87dY2P7qvZsholdicOQzBEU8BNYHo9EqLxwYthOtHu/kgICdEUcBOYXo+EaHwwQ9KJdvfH/6mTEE0BN4Hp9ZghGh/MkHSi3f2REBKiKeAmML0eCdH4YIakE+3uj4SQEE0BN4Hp9bhT1/jovpotq1Fid+IwBEM0BdwEptcjIRofvIekE+3uj4SQEE0BN4Hp9UiIxgczJJ1od38khIRoCrgJTK9HQjQ+mCHpRLv7IyEkRFPATWB6PRKi8cEMSSfa3R8JISGaAm4C0+t9AfQNJzgm7/oIAAAAAElFTkSuQmCC";
91
+ // Editor
92
+
93
+ static MIN_SCALE = 0.25;
94
+ static MAX_SCALE = 4.0;
95
+
96
+ static EVENT_MOUSEMOVE = 0;
97
+ static EVENT_MOUSEWHEEL = 1;
98
+
99
+ static LAST_GROUP_ID = 0;
50
100
 
51
101
  // Node Drawing
52
- static NODE_TITLE_HEIGHT = 24;
53
- static NODE_ROW_HEIGHT = 16;
54
102
 
55
- static NODE_SHAPE_RADIUS = 4;
56
- static NODE_TITLE_RADIUS = [GraphCanvas.NODE_SHAPE_RADIUS, GraphCanvas.NODE_SHAPE_RADIUS, 0, 0];
57
- static NODE_BODY_RADIUS = [GraphCanvas.NODE_SHAPE_RADIUS, GraphCanvas.NODE_SHAPE_RADIUS, GraphCanvas.NODE_SHAPE_RADIUS, GraphCanvas.NODE_SHAPE_RADIUS];
103
+ static NODE_IO_INPUT = 0;
104
+ static NODE_IO_OUTPUT = 1;
58
105
 
59
- static DEFAULT_NODE_TITLE_COLOR = "#4a59b0";
60
- static DEFAULT_NODE_BODY_COLOR = "#111";
106
+ static NODE_TYPES = { };
61
107
 
62
108
  /**
63
109
  * @param {*} options
@@ -66,469 +112,3006 @@ class GraphCanvas {
66
112
 
67
113
  constructor( area, options = {} ) {
68
114
 
69
- GraphCanvas.__instances.push( this );
115
+ GraphEditor.__instances.push( this );
116
+
117
+ area.addSidebar( m => {
118
+ m.add( "Scene", { icon: "fa fa-cube", callback: () => { codeArea.hide(); show( canvas ); } } );
119
+ m.add( "Code", { icon: "fa fa-code", callback: () => { hide( canvas ); codeArea.show(); } } );
120
+ m.add( "Search", { icon: "fa fa-search", bottom: true, callback: () => { } } );
121
+ });
70
122
 
71
123
  this.base_area = area;
72
- this.area = new LX.Area( { className: "lexGraph" } );
124
+ this.area = new LX.Area( { className: "lexgraph" } );
73
125
 
74
- area.root.classList.add('grapharea');
126
+ area.root.classList.add( 'grapharea' );
75
127
 
76
128
  this.root = this.area.root;
129
+ this.root.tabIndex = -1;
77
130
  area.attach( this.root );
78
131
 
79
132
  // Bind resize
133
+
80
134
  area.onresize = ( bb ) => {
81
- this.dom.width = bb.width;
82
- this.dom.height = bb.height;
83
- this._backDirty = true;
84
- this._frontDirty = true;
135
+ console.log(bb);
85
136
  };
86
137
 
87
- this.root.addEventListener( 'keydown', this._processKey.bind(this), true);
88
- this.root.addEventListener( 'mousedown', this._processMouse.bind(this) );
89
- this.root.addEventListener( 'mouseup', this._processMouse.bind(this) );
90
- this.root.addEventListener( 'mousemove', this._processMouse.bind(this) );
91
- this.root.addEventListener( 'click', this._processMouse.bind(this) );
92
- this.root.addEventListener( 'contextmenu', this._processMouse.bind(this) );
93
- this.root.addEventListener( 'focus', this._processFocus.bind(this, true) );
94
- this.root.addEventListener( 'focusout', this._processFocus.bind(this, false) );
95
-
96
- // State
97
-
98
- this.drawAllFrames = false;
99
- this.isFocused = false;
100
- this._backDirty = true;
101
- this._frontDirty = true;
102
-
103
- // Canvas
104
-
105
- this.dom = document.createElement('canvas');
106
- this.dom.width = area.size[0];
107
- this.dom.height = area.size[1];
108
- this.dom.tabIndex = -1;
109
- this.area.attach( this.dom );
110
-
111
- this.frames = 0;
112
- this.fps = 0;
113
- this._lastDrawTime = 0;
114
- this._drawTime = 0;
115
-
116
- this.font = new FontFace("Ubuntu", "url(../data/Ubuntu-Bold.ttf)");
117
- this.font.load().then(
118
- ( font ) => {
119
- document.fonts.add( font );
120
- requestAnimationFrame( this.frame.bind(this) );
121
- },
122
- (err) => {
123
- console.error(err);
138
+ area.addOverlayButtons( [
139
+ {
140
+ name: "Toggle Sidebar",
141
+ icon: "fa fa-table-columns",
142
+ callback: () => this._toggleSideBar(),
124
143
  },
125
- );
126
- }
144
+ // [
145
+ // {
146
+ // name: "Select",
147
+ // icon: "fa fa-arrow-pointer",
148
+ // callback: (value, event) => console.log(value),
149
+ // selectable: true
150
+ // },
151
+ // {
152
+ // name: "Move",
153
+ // icon: "fa-solid fa-arrows-up-down-left-right",
154
+ // img: "https://webglstudio.org/latest/imgs/mini-icon-gizmo.png",
155
+ // callback: (value, event) => console.log(value),
156
+ // selectable: true
157
+ // },
158
+ // {
159
+ // name: "Rotate",
160
+ // icon: "fa-solid fa-rotate-right",
161
+ // callback: (value, event) => console.log(value),
162
+ // selectable: true
163
+ // }
164
+ // ],
165
+ [
166
+ {
167
+ name: "Enable Snapping",
168
+ icon: "fa fa-table-cells",
169
+ callback: () => this._toggleSnapping(),
170
+ selectable: true
171
+ },
172
+ {
173
+ name: 1,
174
+ options: [1, 2, 3],
175
+ callback: value => this._setSnappingValue( value ),
176
+ }
177
+ ],
178
+ [
179
+ {
180
+ name: "Import",
181
+ icon: "fa fa-upload",
182
+ callback: (value, event) => { this.loadGraph( "../../data/graph_sample.json" ); }
183
+ },
184
+ {
185
+ name: "Export",
186
+ icon: "fa fa-diagram-project",
187
+ callback: (value, event) => this.graph.export()
188
+ }
189
+ ]
190
+ ], { float: "htc" } );
191
+
192
+ this.root.addEventListener( 'keydown', this._processKeyDown.bind( this ), true );
193
+ this.root.addEventListener( 'keyup', this._processKeyUp.bind( this ), true );
194
+ this.root.addEventListener( 'mousedown', this._processMouse.bind( this ) );
195
+ this.root.addEventListener( 'mouseup', this._processMouse.bind( this ) );
196
+ this.root.addEventListener( 'mousemove', this._processMouse.bind( this ) );
197
+ this.root.addEventListener( 'mousewheel', this._processMouse.bind(this) );
198
+ this.root.addEventListener( 'mouseleave', this._processMouse.bind(this) );
199
+ this.root.addEventListener( 'click', this._processMouse.bind( this ) );
200
+ this.root.addEventListener( 'contextmenu', this._processMouse.bind( this ) );
201
+ this.root.addEventListener( 'focus', this._processFocus.bind( this, true) );
202
+ this.root.addEventListener( 'focusout', this._processFocus.bind( this, false ) );
203
+
204
+ this.propertiesDialog = new LX.PocketDialog( "Properties", null, {
205
+ size: [ "300px", null ],
206
+ position: [ "8px", "8px" ],
207
+ float: "left",
208
+ class: 'lexgraphpropdialog'
209
+ } );
210
+
211
+ // Avoid closing the dialog on click..
212
+
213
+ this.propertiesDialog.root.addEventListener( "mousedown", function( e ) {
214
+ e.stopImmediatePropagation();
215
+ e.stopPropagation();
216
+ });
127
217
 
128
- static getInstances()
129
- {
130
- return GraphCanvas.__instances;
131
- }
218
+ this.propertiesDialog.root.addEventListener( "mouseup", function( e ) {
219
+ e.stopImmediatePropagation();
220
+ e.stopPropagation();
221
+ });
132
222
 
133
- _processFocus( active ) {
223
+ // Move to root..
224
+ this.root.appendChild( this.propertiesDialog.root );
134
225
 
135
- this.isFocused = active;
226
+ // Editor
227
+
228
+ this._mousePosition = new LX.vec2( 0, 0 );
229
+ this._deltaMousePosition = new LX.vec2( 0, 0 );
230
+ this._snappedDeltaMousePosition = new LX.vec2( 0, 0 );
231
+ this._lastMousePosition = new LX.vec2( 0, 0 );
232
+ this._lastSnappedMousePosition = new LX.vec2( 0, 0 );
233
+
234
+ this._undoSteps = [ ];
235
+ this._redoSteps = [ ];
236
+
237
+ this.keys = { };
238
+
239
+ this._sidebar = area.sections[ 0 ].root;
240
+ this._sidebarActive = false;
241
+
242
+ this._snapToGrid = false;
243
+ this._snapValue = 1.0;
244
+
245
+ this._scale = 1.0;
246
+
247
+ // Nodes and connections
248
+
249
+ this.nodes = { };
250
+ this.variables = { };
251
+
252
+ this.selectedNodes = [ ];
253
+
254
+ this.supportedCastTypes = { };
255
+
256
+ this.addCastType( 'float', 'vec2', ( v ) => { return [ v, v ]; } );
257
+ this.addCastType( 'float', 'vec3', ( v ) => { return [ v, v, v ]; } );
258
+ this.addCastType( 'float', 'vec4', ( v ) => { return [ v, v, v, v ]; } );
259
+ this.addCastType( 'float', 'bool', ( v ) => { return !!v; } );
260
+
261
+ this.addCastType( 'vec4', 'vec3', ( v ) => { v.slice( 0, 3 ); return v; } );
262
+ this.addCastType( 'vec4', 'vec2', ( v ) => { v.slice( 0, 2 ); return v; } );
263
+ this.addCastType( 'vec3', 'vec2', ( v ) => { v.slice( 0, 2 ); return v; } );
264
+
265
+ this.addCastType( 'vec3', 'vec4', ( v ) => { v.push( 1 ); return v; } );
266
+ this.addCastType( 'vec2', 'vec3', ( v ) => { v.push( 1 ); return v; } );
267
+ this.addCastType( 'vec2', 'vec4', ( v ) => { v.push( 0, 1 ); return v; } );
268
+
269
+ this._nodeBackgroundOpacity = options.disableNodeOpacity ? 1.0 : 0.8;
270
+
271
+ this.main = null;
272
+
273
+ // Back pattern
274
+
275
+ const f = 15.0;
276
+ this._patternPosition = new LX.vec2( 0, 0 );
277
+ this._patternSize = new LX.vec2( f );
278
+ this._circlePatternSize = f * 0.04;
279
+ this._circlePatternColor = '#71717a9c';
280
+
281
+ this._generatePattern();
282
+
283
+ // Links
284
+
285
+ this._domLinks = document.createElement( 'div' );
286
+ this._domLinks.classList.add( 'lexgraphlinks' );
287
+ this.root.appendChild( this._domLinks );
288
+
289
+ // Nodes
290
+
291
+ this._domNodes = document.createElement( 'div' );
292
+ this._domNodes.classList.add( 'lexgraphnodes' );
293
+ this.root.appendChild( this._domNodes );
294
+
295
+ window.ge = this;
136
296
  }
137
297
 
138
- _processKey(e) {
298
+ static getInstances() {
139
299
 
140
- var key = e.key ?? e.detail.key;
141
- console.log( key );
300
+ return GraphEditor.__instances;
142
301
  }
143
302
 
144
- _processMouse(e) {
303
+ /**
304
+ * Register a node class so it can be listed when the user wants to create a new one
305
+ * @method registerCustomNode
306
+ * @param {String} type: name of the node and path
307
+ * @param {Class} baseClass class containing the structure of the custom node
308
+ */
145
309
 
146
- if( e.type == 'mousedown' )
147
- {
148
- this.lastMouseDown = LX.getTime();
310
+ static registerCustomNode( type, baseClass ) {
311
+
312
+ if ( !baseClass.prototype ) {
313
+ throw "Cannot register a simple object, it must be a class with a prototype!";
149
314
  }
150
315
 
151
- else if( e.type == 'mouseup' )
316
+ // Get info from path
317
+ const pos = type.lastIndexOf( "/" );
318
+
319
+ baseClass.category = type.substring( 0, pos );
320
+ baseClass.title = baseClass.title ?? type.substring( pos + 1 );
321
+ baseClass.type = type;
322
+
323
+ if( !baseClass.prototype.onExecute )
152
324
  {
153
- if( (LX.getTime() - this.lastMouseDown) < 300 ) {
154
- this._processClick(e);
155
- }
325
+ console.warn( `GraphNode [${ this.title }] does not have a callback attached.` );
156
326
  }
157
327
 
158
- else if( e.type == 'mousemove' )
159
- {
160
-
328
+ const prev = GraphEditor.NODE_TYPES[ type ];
329
+ if(prev) {
330
+ console.warn( `Replacing node type [${ type }]` );
161
331
  }
162
332
 
163
- else if ( e.type == 'click' ) // trip
164
- {
165
- switch( e.detail )
166
- {
167
- case LX.MOUSE_DOUBLE_CLICK:
168
- break;
169
- case LX.MOUSE_TRIPLE_CLICK:
170
- break;
171
- }
333
+ GraphEditor.NODE_TYPES[ type ] = baseClass;
334
+
335
+ // Some callbacks..
336
+
337
+ if ( this.onNodeTypeRegistered ) {
338
+ this.onCustomNodeRegistered( type, baseClass);
172
339
  }
173
340
 
174
- else if ( e.type == 'contextmenu' ) {
175
- e.preventDefault()
176
- this._processContextMenu( e );
341
+ if ( prev && this.onNodeTypeReplaced ) {
342
+ this.onNodeTypeReplaced( type, baseClass, prev );
177
343
  }
178
344
  }
179
345
 
180
- _processClick( e ) {
346
+ /**
347
+ * Create a node of a given type with a name. The node is not attached to any graph yet.
348
+ * @method createNode
349
+ * @param {String} type full name of the node class. p.e. "math/sin"
350
+ * @param {String} title a name to distinguish from other nodes
351
+ * @param {Object} options Store node options
352
+ */
181
353
 
182
-
183
- }
354
+ static addNode( type, title, options ) {
184
355
 
185
- _processContextMenu( e ) {
186
-
187
- LX.addContextMenu( "Test", e, m => {
188
- m.add( "option 1", () => { } );
189
- m.add( "option 2", () => { } );
190
- });
191
- }
356
+ var baseClass = GraphEditor.NODE_TYPES[ type ];
357
+
358
+ if (!baseClass) {
359
+ console.warn( `GraphNode type [${ type }] not registered.` );
360
+ return null;
361
+ }
362
+
363
+ title = title ?? baseClass.title;
364
+
365
+ const node = new baseClass( title );
192
366
 
193
- _forceDraw() {
367
+ if( node.onCreate )
368
+ node.onCreate();
194
369
 
195
- this._backDirty = true;
196
- this._frontDirty = true;
370
+ node.type = type;
371
+ node.title = title;
372
+ node.position = new LX.vec2( 0, 0 );
373
+ node.color = null;
374
+
375
+ // Extra options
376
+ if ( options ) {
377
+ for (var i in options) {
378
+ node[ i ] = options[ i ];
379
+ }
380
+ }
381
+
382
+ if ( node.onNodeCreated ) {
383
+ node.onNodeCreated();
384
+ }
385
+
386
+ return node;
197
387
  }
198
388
 
199
389
  /**
200
390
  * @method setGraph
201
- * @param {Graph} graph:
391
+ * @param {Graph} graph
202
392
  */
203
393
 
204
394
  setGraph( graph ) {
205
395
 
396
+ this.clear();
397
+
206
398
  this.graph = graph;
399
+
400
+ if( !this.graph.nodes )
401
+ {
402
+ console.warn( 'Graph does not contain any node!' );
403
+ return;
404
+ }
405
+
406
+ for( let node of this.graph.nodes )
407
+ {
408
+ this._createNodeDOM( node );
409
+ }
410
+
411
+ for( let linkId in this.graph.links )
412
+ {
413
+ const links = this.graph.links[ linkId ];
414
+
415
+ for( let link of links )
416
+ {
417
+ this._createLink( link );
418
+ }
419
+ }
420
+
421
+ // TODO: REMOVE THIS (DEBUG)
422
+ this.start();
423
+ //
207
424
  }
208
425
 
209
426
  /**
210
- * @method clear
427
+ * @method loadGraph
428
+ * @param {Graph} graph
211
429
  */
212
430
 
213
- clear( ) {
431
+ loadGraph( url, callback ) {
432
+
433
+ const onComplete = ( json ) => {
434
+
435
+ var graph = new Graph();
436
+ graph.configure( json );
214
437
 
438
+ this.setGraph( graph );
439
+
440
+ if( callback )
441
+ callback( graph );
442
+ }
443
+
444
+ const onError = (v) => console.error(v);
445
+
446
+ LX.requestJSON( url, onComplete, onError );
215
447
  }
216
448
 
217
449
  /**
218
- * @method frame
450
+ * @method clear
219
451
  */
220
452
 
221
- frame() {
453
+ clear() {
454
+
455
+ this._domNodes.innerHTML = "";
456
+ this._domLinks.innerHTML = "";
457
+
458
+ this.nodes = { };
459
+ }
460
+
461
+ setVariable( name, value ) {
462
+
463
+ this.variables[ name ] = value;
464
+ }
222
465
 
223
- // this.update();
224
- this.draw();
466
+ getVariable( name ) {
225
467
 
226
- requestAnimationFrame( this.frame.bind(this) );
468
+ return this.variables[ name ];
227
469
  }
228
470
 
471
+ propagateEventToAllNodes( eventName, params ) {
472
+
473
+ if( !this.graph )
474
+ return;
475
+
476
+ for ( let node of this.graph.nodes )
477
+ {
478
+ if( !node[ eventName ] )
479
+ continue;
480
+
481
+ node[ eventName ].apply( this, params );
482
+ }
483
+ };
484
+
229
485
  /**
230
- * @method update
486
+ * @method addCastType
487
+ * @param {String} type: Type to cast
488
+ * @param {String} targetType: Types to be casted from original type
489
+ * @param {Function} fn: Function to know how to cast
231
490
  */
232
491
 
233
- update() {
234
-
235
- console.log("Update");
492
+ addCastType( type, targetType, fn ) {
493
+
494
+ this.supportedCastTypes[ type + '@' + targetType ] = fn;
236
495
  }
237
496
 
238
497
  /**
239
- * @method draw
498
+ * @method unSelectAll
240
499
  */
241
500
 
242
- draw() {
501
+ unSelectAll( keepPropDialog ) {
243
502
 
244
- if (!this.dom || !this.dom.width || !this.dom.height)
245
- return;
503
+ this._domNodes.querySelectorAll( '.lexgraphnode' ).forEach( v => v.classList.remove( 'selected' ) );
246
504
 
247
- // Count Fps
248
- var now = LX.getTime();
249
- this._drawTime = (now - this._lastDrawTime) * 0.001;
250
- this._lastDrawTime = now;
505
+ this.selectedNodes.length = 0;
251
506
 
252
- // if (this.graph) {
253
- // this.ds.computeVisibleArea(this.viewport);
254
- // }
507
+ if( !keepPropDialog )
508
+ this._togglePropertiesDialog( false );
509
+ }
255
510
 
256
- const forceDraw = this.drawAllFrames || (this._backDirty || this._frontDirty);
511
+ _createNodeDOM( node ) {
257
512
 
258
- if ( forceDraw )
513
+ node.editor = this;
514
+
515
+ var nodeContainer = document.createElement( 'div' );
516
+ nodeContainer.classList.add( 'lexgraphnode' );
517
+ nodeContainer.style.left = "0";
518
+ nodeContainer.style.top = "0";
519
+
520
+ this._translateNode( nodeContainer, node.position );
521
+
522
+ var color;
523
+
524
+ // Get color from type if color if not manually specified
525
+ if( node.type && GraphEditor.NODE_TYPES[ node.type ] )
259
526
  {
260
- if( this._backDirty )
261
- this._drawBack();
262
- if ( this._frontDirty )
263
- this._drawFront();
527
+ const category = node.constructor.category;
528
+ nodeContainer.classList.add( category );
264
529
  }
265
530
 
266
- this.fps = this._drawTime ? (1.0 / this._drawTime) : 0;
267
- this.frames += 1;
268
- }
531
+ // Update with manual color
269
532
 
270
- _drawBack() {
533
+ color = node.color ?? color;
271
534
 
272
- console.log( "_drawBack" );
535
+ if( color )
536
+ {
537
+ // RGB
538
+ if( color.constructor == Array )
539
+ {
540
+ color = color.join( ',' );
541
+ }
542
+ // Hex color..
543
+ else
544
+ {
545
+ color = LX.hexToRgb( color );
546
+ color.forEach( ( v, i ) => color[ i ] = v * 255 );
547
+ }
273
548
 
274
- var ctx = this.dom.getContext("2d");
549
+ nodeContainer.style.backgroundColor = "rgba(" + color + ", " + this._nodeBackgroundOpacity + ")";
550
+ }
275
551
 
276
- if ( !GraphCanvas.BACK_IMAGE_SRC )
277
- return;
552
+ nodeContainer.addEventListener( 'mousedown', e => {
278
553
 
279
- ctx.imageSmoothingEnabled = false;
554
+ // Only for left click..
555
+ if( e.button != LX.MOUSE_LEFT_CLICK )
556
+ return;
280
557
 
281
- if ( !this._backImage ) {
282
- this._backImage = new Image();
283
- this._backImage.src = GraphCanvas.BACK_IMAGE_SRC;
284
- this._backImage.onload = this._forceDraw.bind(this);
285
- }
558
+ if( e.altKey )
559
+ {
560
+ this._unSelectNode( nodeContainer );
561
+ }
562
+ else
563
+ {
564
+ if( this.selectedNodes.length > 1 && ( !e.ctrlKey && !e.shiftKey ) )
565
+ {
566
+ this.unSelectAll( true );
567
+ }
568
+
569
+ if( !nodeContainer.classList.contains( 'selected' ) )
570
+ {
571
+ this._selectNode( nodeContainer, ( e.ctrlKey || e.shiftKey ) );
572
+ }
573
+ }
574
+ } );
286
575
 
287
- if ( !this._pattern && this._backImage.width > 0) {
288
- this._pattern = ctx.createPattern(this._backImage, "repeat");
289
- }
576
+ nodeContainer.addEventListener( 'contextmenu', e => {
290
577
 
291
- // Draw background
578
+ e.preventDefault();
579
+ e.stopPropagation();
580
+ e.stopImmediatePropagation();
292
581
 
293
- if (this._pattern) {
294
- ctx.fillStyle = this._pattern;
295
- ctx.fillRect(0, 0, this.dom.width, this.dom.height);
296
- ctx.fillStyle = "transparent";
297
- }
298
-
299
- ctx.globalAlpha = 1.0;
300
- ctx.imageSmoothingEnabled = true;
582
+ LX.addContextMenu(null, e, m => {
301
583
 
302
- // Draw node connections
584
+ m.add( "Copy", () => {
585
+ // TODO
586
+ // ...
587
+ } );
303
588
 
304
- this._drawConnections();
589
+ m.add( "Paste", () => {
590
+ // TODO
591
+ // ...
592
+ } );
305
593
 
306
- this._backDirty = false;
307
- }
594
+ m.add( "" );
308
595
 
309
- _drawFront() {
596
+ m.add( "Delete", () => {
597
+ this._deleteNode( nodeContainer.dataset[ 'id' ] );
598
+ } );
599
+ });
600
+ } );
310
601
 
311
- console.log( "_drawFront" );
602
+ nodeContainer.addEventListener( 'dblclick', e => {
312
603
 
313
- let nodes = this._getVisibleNodes();
604
+ // Only for left click..
605
+ if( e.button != LX.MOUSE_LEFT_CLICK )
606
+ return;
607
+ } );
314
608
 
315
- for( let node of nodes )
316
- {
317
- this._drawNode( node );
318
- }
609
+ // Title header
610
+ var nodeHeader = document.createElement( 'div' );
611
+ nodeHeader.classList.add( 'lexgraphnodeheader' );
612
+ nodeHeader.innerText = node.title;
613
+ nodeContainer.appendChild( nodeHeader );
319
614
 
320
- this._frontDirty = false;
321
- }
615
+ // Properties
616
+ // if( node.properties.length && node.constructor.category == 'inputs' )
617
+ // {
618
+ // var nodeProperties = document.createElement( 'div' );
619
+ // nodeProperties.classList.add( 'lexgraphnodeproperties' );
620
+
621
+ // for( let p of node.properties )
622
+ // {
623
+ // var panel = new LX.Panel();
624
+
625
+ // p.signal = "@" + LX.UTILS.uidGenerator() + node.title;
626
+
627
+ // switch( p.type )
628
+ // {
629
+ // case 'float':
630
+ // case 'int':
631
+ // panel.addNumber( p.name, p.value, (v) => p.value = v, { signal: p.signal } );
632
+ // break;
633
+ // case 'string':
634
+ // panel.addText( p.name, p.value, (v) => p.value = v, { signal: p.signal } );
635
+ // break;
636
+ // }
637
+
638
+ // // var prop = document.createElement( 'div' );
639
+ // // prop.innerText = p.type;
640
+ // // prop.classList.add( 'lexgraphnodeproperty' );
641
+ // nodeProperties.appendChild( panel.root );
642
+ // }
643
+
644
+ // nodeContainer.appendChild( nodeProperties );
645
+ // }
322
646
 
323
- _resetCanvasShadows( ctx ) {
647
+ // Inputs and outputs
648
+ var nodeIO = document.createElement( 'div' );
649
+ nodeIO.classList.add( 'lexgraphnodeios' );
650
+ nodeContainer.appendChild( nodeIO );
324
651
 
325
- ctx.shadowOffsetX = 0;
326
- ctx.shadowOffsetY = 0;
327
- ctx.shadowBlur = 0;
328
- ctx.shadowColor = "rgba(0,0,0,0)";
329
- }
652
+ const hasInputs = node.inputs && node.inputs.length;
653
+ const hasOutputs = node.outputs && node.outputs.length;
654
+
655
+ // Inputs
656
+ {
657
+ var nodeInputs = null;
330
658
 
331
- _computeNodeSize( node ) {
659
+ if( node.inputs && node.inputs.length )
660
+ {
661
+ nodeInputs = document.createElement( 'div' );
662
+ nodeInputs.classList.add( 'lexgraphnodeinputs' );
663
+ nodeInputs.style.width = hasOutputs ? "50%" : "100%";
664
+ nodeIO.appendChild( nodeInputs );
665
+ }
332
666
 
333
- const ctx = this.dom.getContext("2d");
334
- var textMetrics = ctx.measureText( node.name );
667
+ for( let i of node.inputs )
668
+ {
669
+ if( !i.type )
670
+ {
671
+ console.warn( `Missing type for node [${ node.title }], skipping...` );
672
+ continue;
673
+ }
674
+
675
+ var input = document.createElement( 'div' );
676
+ input.className = 'lexgraphnodeio ioinput';
677
+ input.dataset[ 'index' ] = nodeInputs.childElementCount;
678
+
679
+ var type = document.createElement( 'span' );
680
+ type.className = 'io__type input ' + i.type;
681
+ type.innerHTML = '<span>' + i.type[ 0 ].toUpperCase() + '</span>';
682
+ input.appendChild( type );
683
+
684
+ var typeDesc = document.createElement( 'span' );
685
+ typeDesc.className = 'io__typedesc input ' + i.type;
686
+ typeDesc.innerHTML = i.type;
687
+ input.appendChild( typeDesc );
688
+
689
+ if( i.name )
690
+ {
691
+ var name = document.createElement( 'span' );
692
+ name.classList.add( 'io__name' );
693
+ name.innerText = i.name;
694
+ input.appendChild( name );
695
+ }
696
+
697
+ nodeInputs.appendChild( input );
698
+ }
699
+ }
335
700
 
336
- let sX = 32 + textMetrics.width * 1.475;
701
+ // Outputs
702
+ {
703
+ var nodeOutputs = null;
337
704
 
338
- const rows = Math.max(1, Math.max(node.inputs.length, node.outputs.length));
339
- let sY = rows * GraphCanvas.NODE_ROW_HEIGHT + GraphCanvas.NODE_TITLE_HEIGHT;
705
+ if( node.outputs && node.outputs.length )
706
+ {
707
+ nodeOutputs = document.createElement( 'div' );
708
+ nodeOutputs.classList.add( 'lexgraphnodeoutputs' );
709
+ nodeOutputs.style.width = hasInputs ? "50%" : "100%";
710
+ nodeIO.appendChild( nodeOutputs );
711
+ }
340
712
 
341
- return [sX, sY];
342
- }
713
+ for( let o of node.outputs )
714
+ {
715
+ if( !o.type )
716
+ {
717
+ console.warn( `Missing type for node [${ node.title }], skipping...` );
718
+ }
719
+
720
+ var output = document.createElement( 'div' );
721
+ output.className = 'lexgraphnodeio iooutput';
722
+ output.dataset[ 'index' ] = nodeOutputs.childElementCount;
723
+
724
+ if( o.name )
725
+ {
726
+ var name = document.createElement( 'span' );
727
+ name.classList.add( 'io__name' );
728
+ name.innerText = o.name;
729
+ output.appendChild( name );
730
+ }
731
+
732
+ var type = document.createElement( 'span' );
733
+ type.className = 'io__type output ' + o.type;
734
+ type.innerHTML = '<span>' + o.type[ 0 ].toUpperCase() + '</span>';
735
+ output.appendChild( type );
736
+
737
+ var typeDesc = document.createElement( 'span' );
738
+ typeDesc.className = 'io__typedesc output ' + o.type;
739
+ typeDesc.innerHTML = o.type;
740
+ output.appendChild( typeDesc );
741
+
742
+ nodeOutputs.appendChild( output );
743
+ }
744
+ }
343
745
 
344
- _drawConnections() {
746
+ // Move nodes
345
747
 
346
- console.log( "_drawConnections" );
748
+ LX.makeDraggable( nodeContainer, {
749
+ onMove: this._onMoveNodes.bind( this ),
750
+ onDragStart: this._onDragNode.bind( this )
751
+ } );
347
752
 
348
- const ctx = this.dom.getContext("2d");
753
+ // Manage links
349
754
 
350
- let nodes = this._getVisibleNodes();
755
+ nodeIO.querySelectorAll( '.lexgraphnodeio' ).forEach( el => {
351
756
 
352
- let start = { x: 50, y: 20 };
353
- let cp1 = { x: 230, y: 30 };
354
- let cp2 = { x: 150, y: 80 };
355
- let end = { x: 250, y: 100 };
757
+ el.addEventListener( 'mousedown', e => {
356
758
 
357
- // Cubic Bézier curve
358
- ctx.beginPath();
359
- ctx.moveTo(start.x, start.y);
360
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
361
- ctx.stroke();
759
+ // Only for left click..
760
+ if( e.button != LX.MOUSE_LEFT_CLICK )
761
+ return;
362
762
 
363
- // for( let node of nodes )
364
- // {
365
- // // Discard nodes without inputs...
366
- // if (!node.inputs || !node.inputs.length) {
367
- // continue;
368
- // }
763
+ this.lastMouseDown = LX.getTime();
764
+
765
+ this._generatingLink = {
766
+ index: parseInt( el.dataset[ 'index' ] ),
767
+ io: el,
768
+ ioType: el.classList.contains( 'ioinput' ) ? GraphEditor.NODE_IO_INPUT : GraphEditor.NODE_IO_OUTPUT,
769
+ domEl: nodeContainer
770
+ };
369
771
 
370
- // for (let input of node.inputs) {
772
+ e.stopPropagation();
773
+ e.stopImmediatePropagation();
774
+ } );
371
775
 
776
+ el.addEventListener( 'mouseup', e => {
372
777
 
373
- // }
374
- // }
375
- }
778
+ // Single click..
779
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
780
+ delete this._generatingLink;
781
+ return;
782
+ }
376
783
 
377
- _drawNode( node ) {
784
+ if( this._generatingLink )
785
+ {
786
+ // Check for IO
787
+ if( !this._onLink( e ) )
788
+ {
789
+ // Delete entire SVG if not a successful connection..
790
+ deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
791
+ }
378
792
 
379
- console.log( node.name );
793
+ delete this._generatingLink;
794
+ }
380
795
 
381
- // Process some attributes
382
- node.size = node.size ?? this._computeNodeSize( node );
383
- node.color = node.color ?? GraphCanvas.DEFAULT_NODE_BODY_COLOR;
384
- node.titleColor = node.titleColor ?? GraphCanvas.DEFAULT_NODE_TITLE_COLOR;
796
+ e.stopPropagation();
797
+ e.stopImmediatePropagation();
798
+ } );
385
799
 
386
- let [pX, pY] = node.position;
387
- let [sX, sY] = node.size;
800
+ el.addEventListener( 'click', e => {
388
801
 
389
- const ctx = this.dom.getContext("2d");
390
- const offsetY = GraphCanvas.NODE_TITLE_HEIGHT;
802
+ if( !el.links )
803
+ return;
391
804
 
392
- // Body
805
+ const nodeId = nodeContainer.dataset[ 'id' ];
393
806
 
394
- ctx.shadowBlur = 8;
395
- ctx.shadowColor = "#000";
807
+ this._deleteLinks( nodeId, el );
808
+ } );
396
809
 
397
- ctx.beginPath();
398
- ctx.fillStyle = node.color;
399
- ctx.roundRect( pX, pY, sX, sY, GraphCanvas.NODE_BODY_RADIUS );
400
- ctx.fill();
810
+ } );
401
811
 
402
- this._resetCanvasShadows( ctx );
812
+ const id = node.id ?? node.title.toLowerCase().replaceAll( /\s/g, '-' ) + '-' + LX.UTILS.uidGenerator();
813
+ this.nodes[ id ] = { data: node, dom: nodeContainer };
814
+
815
+ node.id = id;
816
+ nodeContainer.dataset[ 'id' ] = id;
403
817
 
404
- // Draw border
405
- ctx.beginPath();
406
- ctx.strokeStyle = "#555";
407
- ctx.roundRect( pX, pY, sX, sY, GraphCanvas.NODE_BODY_RADIUS );
408
- ctx.stroke();
818
+ this._domNodes.appendChild( nodeContainer );
409
819
 
410
- // Title
820
+ // Only 1 main per graph!
821
+ if( node.title == 'Main' )
822
+ {
823
+ this.main = nodeContainer;
824
+ }
411
825
 
412
- ctx.beginPath();
413
- var titleGrd = ctx.createLinearGradient(pX, pY, pX + sX, pY + offsetY);
414
- titleGrd.addColorStop(0, node.color);
415
- titleGrd.addColorStop(1, node.titleColor);
826
+ return nodeContainer;
827
+ }
416
828
 
417
- ctx.fillStyle = titleGrd;
418
- ctx.roundRect( pX + 1, pY, sX - 2, offsetY, GraphCanvas.NODE_TITLE_RADIUS );
419
- ctx.fill();
829
+ _getAllDOMNodes( includeGroups ) {
830
+
831
+ if( includeGroups )
832
+ return this._domNodes.childNodes;
420
833
 
421
- ctx.font = "14px Ubuntu";
422
- ctx.fillStyle = "#ddd";
423
- ctx.fillText( node.name, pX + 16, pY + offsetY * 0.75);
834
+ return Array.from( this._domNodes.childNodes ).filter( v => v.classList.contains( 'lexgraphnode' ) );
424
835
  }
425
836
 
426
- _getVisibleNodes() {
837
+ _onMoveNodes( target ) {
427
838
 
428
- if( !this.graph )
839
+ let dT = this._snapToGrid ? this._snappedDeltaMousePosition : this._deltaMousePosition;
840
+ dT.div( this._scale, dT);
841
+
842
+ for( let nodeId of this.selectedNodes )
429
843
  {
430
- console.warn( "No graph set" );
431
- return [];
844
+ const el = this._getNodeDOMElement( nodeId );
845
+
846
+ this._translateNode( el, dT );
847
+
848
+ this._updateNodeLinks( nodeId );
432
849
  }
850
+ }
433
851
 
434
- // TODO: Return the ones in the viewport
435
- return this.graph.nodes;
852
+ _onDragNode( target, e ) {
853
+
854
+ if( !e.shiftKey )
855
+ return;
856
+
857
+ this._cloneNodes();
436
858
  }
437
859
 
438
- }
860
+ _onMoveGroup( target ) {
439
861
 
440
- LX.GraphCanvas = GraphCanvas;
862
+ // Move nodes inside the group
441
863
 
442
- /**
443
- * @class Graph
444
- */
864
+ const groupNodeIds = target.nodes;
445
865
 
446
- class Graph {
866
+ if( !groupNodeIds )
867
+ return;
447
868
 
448
- /**
449
- * @param {*} options
450
- *
451
- */
869
+ let dT = this._snapToGrid ? this._snappedDeltaMousePosition : this._deltaMousePosition;
870
+ dT.div( this._scale, dT);
452
871
 
453
- constructor( options = {} ) {
872
+ this._translateNode( target, dT );
454
873
 
455
- // Nodes
874
+ for( let nodeId of groupNodeIds )
875
+ {
876
+ const el = this._getNodeDOMElement( nodeId );
456
877
 
457
- this.nodes = [
458
- new GraphNode({
459
- name: "Node 1",
460
- xsize: [120, 100],
461
- position: [200, 200],
462
- inputs: [
463
- {
464
- name: "Speed",
465
- type: "number"
466
- },
467
- {
468
- name: "Offset",
469
- type: "number"
470
- }
471
- ],
472
- outputs: [
473
- {
474
- name: "Speed",
475
- type: "number"
476
- },
477
- {
478
- name: "Offset",
479
- type: "number"
480
- },
481
- {
482
- name: "Loop",
483
- type: "bool"
484
- }
485
- ]
486
- }),
487
- new GraphNode({
488
- name: "Node 2",
489
- size: [120, 100],
490
- position: [500, 350],
491
- inputs: [],
492
- outputs: []
493
- })
494
- ];
878
+ this._translateNode( el, dT );
879
+
880
+ this._updateNodeLinks( nodeId );
881
+ }
495
882
  }
496
- }
497
883
 
498
- LX.Graph = Graph;
884
+ _onDragGroup( target ) {
499
885
 
500
- /**
501
- * @class GraphNode
502
- */
886
+ // Get nodes inside the group to be moved
503
887
 
504
- class GraphNode {
888
+ const group_bb = this._getBoundingFromGroup( target );
505
889
 
506
- /**
507
- * @param {*} options
508
- *
509
- */
890
+ const groupNodeIds = [ ];
510
891
 
511
- constructor( options = {} ) {
892
+ for( let dom of this._getAllDOMNodes() )
893
+ {
894
+ const x = parseFloat( dom.style.left );
895
+ const y = parseFloat( dom.style.top );
896
+ const node_bb = new BoundingBox( new LX.vec2( x, y ), new LX.vec2( dom.offsetWidth - 6, dom.offsetHeight - 6 ) );
512
897
 
513
- this.name = options.name ?? "Unnamed";
514
- this.size = options.size;
515
- this.position = options.position ?? [0, 0];
516
-
517
- this.inputs = options.inputs ?? [];
518
- this.outputs = options.outputs ?? [];
898
+ if( !group_bb.inside( node_bb ) )
899
+ continue;
900
+
901
+ groupNodeIds.push( dom.dataset[ 'id' ] );
902
+ }
903
+
904
+ target.nodes = groupNodeIds;
519
905
  }
520
906
 
521
- computeSize() {
907
+ _selectNode( dom, multiSelection, forceOrder = true ) {
522
908
 
523
- let sX = 16 + this.name.length * 10;
909
+ if( !multiSelection )
910
+ this.unSelectAll( true );
524
911
 
525
- const rows = Math.max(1, Math.max(this.inputs.length, this.outputs.length));
526
- let sY = rows * GraphCanvas.NODE_ROW_HEIGHT + GraphCanvas.NODE_TITLE_HEIGHT;
912
+ dom.classList.add( 'selected' );
527
913
 
528
- return [sX, sY];
529
- }
530
- }
914
+ const id = dom.dataset[ 'id' ];
531
915
 
532
- LX.GraphNode = GraphNode;
916
+ this.selectedNodes.push( id );
917
+
918
+ if( forceOrder )
919
+ {
920
+ // Reorder nodes to draw on top..
921
+ this._domNodes.appendChild( dom );
922
+ }
923
+
924
+ const node = this.nodes[ id ].data;
925
+
926
+ this.propertiesDialog.setTitle( node.title );
927
+
928
+ var panel = this.propertiesDialog.panel;
929
+ panel.clear();
930
+
931
+ // Add description
932
+ if( node.constructor.description )
933
+ {
934
+ panel.addText( null, node.constructor.description, null, { disabled: true } );
935
+ }
936
+
937
+ // Allow change name if input
938
+ if( node.constructor.category == 'inputs' )
939
+ {
940
+ panel.addText( 'Name', node.title, (v) => {
941
+ node.title = v;
942
+ dom.querySelector( '.lexgraphnodeheader' ).innerText = v;
943
+ } );
944
+ }
945
+
946
+ for( let p of node.properties )
947
+ {
948
+ switch( p.type )
949
+ {
950
+ case 'float':
951
+ case 'int':
952
+ panel.addNumber( p.name, p.value, (v) => { p.value = v } );
953
+ break;
954
+ case 'string':
955
+ panel.addText( p.name, p.value, (v) => { p.value = v } );
956
+ break;
957
+ case 'vec2':
958
+ panel.addVector2( p.name, p.value, (v) => { p.value = v } );
959
+ break;
960
+ case 'vec3':
961
+ panel.addVector3( p.name, p.value, (v) => { p.value = v } );
962
+ break;
963
+ case 'vec4':
964
+ panel.addVector4( p.name, p.value, (v) => { p.value = v } );
965
+ break;
966
+ case 'select':
967
+ panel.addDropdown( p.name, p.options, p.value, (v) => { p.value = v } );
968
+ break;
969
+ }
970
+ }
971
+
972
+ this._togglePropertiesDialog( true );
973
+ }
974
+
975
+ _unSelectNode( dom ) {
976
+
977
+ dom.classList.remove( 'selected' );
978
+
979
+ // Delete from selected..
980
+ const idx = this.selectedNodes.indexOf( dom.dataset[ 'id' ] );
981
+ this.selectedNodes.splice( idx, 1 );
982
+
983
+ if( !this.selectedNodes.length )
984
+ this._togglePropertiesDialog( false );
985
+ }
986
+
987
+ _translateNode( dom, deltaTranslation ) {
988
+
989
+ const translation = deltaTranslation.add( new LX.vec2( parseFloat( dom.style.left ), parseFloat( dom.style.top ) ) );
990
+
991
+ if( this._snapToGrid && dom.mustSnap )
992
+ {
993
+ const snapSize = this._patternSize.x * this._snapValue * this._snapValue;
994
+ translation.x = Math.floor( translation.x / snapSize ) * snapSize;
995
+ translation.y = Math.floor( translation.y / snapSize ) * snapSize;
996
+ dom.mustSnap = false;
997
+ }
998
+
999
+ dom.style.left = ( translation.x ) + "px";
1000
+ dom.style.top = ( translation.y ) + "px";
1001
+ }
1002
+
1003
+ _deleteNode( nodeId ) {
1004
+
1005
+ const el = this._getNodeDOMElement( nodeId );
1006
+
1007
+ console.assert( el );
1008
+
1009
+ if( el == this.main )
1010
+ {
1011
+ console.warn( `Can't delete MAIN node!` );
1012
+ return;
1013
+ }
1014
+
1015
+ deleteElement( el );
1016
+
1017
+ delete this.nodes[ nodeId ];
1018
+
1019
+ // Delete connected links..
1020
+
1021
+ for( let key in this.graph.links )
1022
+ {
1023
+ if( !key.includes( nodeId ) )
1024
+ continue;
1025
+
1026
+ const aIdx = key.indexOf( '@' );
1027
+ const targetIsInput = key.substring( aIdx + 1 ) != nodeId;
1028
+
1029
+ // Remove the connection from the other before deleting..
1030
+
1031
+ const numLinks = this.graph.links[ key ].length;
1032
+
1033
+ for( var i = 0; i < numLinks; ++i )
1034
+ {
1035
+ var link = this.graph.links[ key ][ i ];
1036
+
1037
+ deleteElement( link.path.parentElement );
1038
+
1039
+ const targetNodeId = targetIsInput ? link.inputNode : link.outputNode;
1040
+
1041
+ const targetNodeDOM = this._getNodeDOMElement( targetNodeId );
1042
+ const ios = targetNodeDOM.querySelector( targetIsInput ? '.lexgraphnodeinputs' : '.lexgraphnodeoutputs' );
1043
+ const io = ios.childNodes[ targetIsInput ? link.inputIdx : link.outputIdx ];
1044
+
1045
+ const ioIndex = targetIsInput ? link.outputIdx : link.inputIdx;
1046
+ const nodelinkidx = io.links[ ioIndex ].indexOf( nodeId );
1047
+ io.links[ ioIndex ].splice( nodelinkidx, 1 );
1048
+
1049
+ // Unique link, so it's done..
1050
+ if( targetIsInput )
1051
+ {
1052
+ delete io.dataset[ 'active' ];
1053
+ }
1054
+
1055
+ // Check if any link left in case of output
1056
+ else
1057
+ {
1058
+ var active = false;
1059
+ for( var links of io.links )
1060
+ for( var j of links ){
1061
+ console.log(j)
1062
+ active |= ( !!j );
1063
+ }
1064
+ if( !active )
1065
+ delete io.dataset[ 'active' ];
1066
+ }
1067
+ }
1068
+
1069
+ delete this.graph.links[ key ];
1070
+ }
1071
+ }
1072
+
1073
+ _cloneNodes() {
1074
+
1075
+ // Clone all selected nodes
1076
+ const selectedIds = LX.deepCopy( this.selectedNodes );
1077
+
1078
+ this.unSelectAll();
1079
+
1080
+ for( let nodeId of selectedIds )
1081
+ {
1082
+ const nodeInfo = this.nodes[ nodeId ];
1083
+ if( !nodeInfo )
1084
+ return;
1085
+
1086
+ const el = this._getNodeDOMElement( nodeId );
1087
+ const data = nodeInfo.data;
1088
+ const newNode = GraphEditor.addNode( data.type );
1089
+ const newDom = this._createNodeDOM( newNode );
1090
+
1091
+ this._translateNode( newDom, this._getNodePosition( el ) );
1092
+
1093
+ this._selectNode( newDom, true );
1094
+
1095
+ this.graph.nodes.push( newNode );
1096
+ }
1097
+ }
1098
+
1099
+ // This is in pattern space!
1100
+ _getNodePosition( dom ) {
1101
+
1102
+ return new LX.vec2( parseFloat( dom.style.left ), parseFloat( dom.style.top ) );
1103
+ }
1104
+
1105
+ _getNodeDOMElement( nodeId ) {
1106
+
1107
+ return this.nodes[ nodeId ] ? this.nodes[ nodeId ].dom : null;
1108
+ }
1109
+
1110
+ _getLinks( nodeSrcId, nodeDstId ) {
1111
+
1112
+ const str = nodeSrcId + '@' + nodeDstId;
1113
+ return this.graph.links[ str ];
1114
+ }
1115
+
1116
+ _deleteLinks( nodeId, io ) {
1117
+
1118
+ const isInput = io.classList.contains( 'ioinput' );
1119
+ const srcIndex = parseInt( io.dataset[ 'index' ] );
1120
+
1121
+ if( isInput ) // Only one "link to output" to delete
1122
+ {
1123
+ let targetIndex;
1124
+
1125
+ const targets = io.links.filter( (v, i) => { targetIndex = i; return v !== undefined; } )[ 0 ];
1126
+ const targetId = targets[ 0 ];
1127
+
1128
+ // It has been deleted..
1129
+ if( !targetId )
1130
+ return;
1131
+
1132
+ var links = this._getLinks( targetId, nodeId );
1133
+
1134
+ var linkIdx = links.findIndex( i => ( i.inputIdx == srcIndex && i.outputIdx == targetIndex ) );
1135
+ deleteElement( links[ linkIdx ].path.parentElement );
1136
+ links.splice( linkIdx, 1 );
1137
+
1138
+ // Input has no longer any connected link
1139
+
1140
+ delete io.links;
1141
+ delete io.dataset[ 'active' ];
1142
+
1143
+ // Remove a connection from the target connections
1144
+
1145
+ const targetDOM = this._getNodeDOMElement( targetId );
1146
+ const ios = targetDOM.querySelector( '.lexgraphnodeoutputs' );
1147
+ const targetIO = ios.childNodes[ targetIndex ];
1148
+
1149
+ const idx = targetIO.links[ srcIndex ].findIndex( v => v == nodeId );
1150
+ targetIO.links[ srcIndex ].splice( idx, 1 );
1151
+
1152
+ let active = false;
1153
+
1154
+ for( var ls of targetIO.links )
1155
+ {
1156
+ if( !ls ) continue;
1157
+ // Check links left per io
1158
+ active |= ls.reduce( c => c !== undefined, 0 );
1159
+ }
1160
+
1161
+ if( !active )
1162
+ {
1163
+ delete targetIO.links;
1164
+ delete targetIO.dataset[ 'active' ];
1165
+ }
1166
+ }
1167
+ else // Delete ALL "to input links"
1168
+ {
1169
+
1170
+ const numLinks = io.links.length;
1171
+
1172
+ for( let targetIndex = 0; targetIndex < numLinks; ++targetIndex )
1173
+ {
1174
+ const targets = io.links[ targetIndex ];
1175
+
1176
+ if( !targets )
1177
+ continue;
1178
+
1179
+ for( let it = ( targets.length - 1 ); it >= 0 ; --it )
1180
+ {
1181
+ const targetId = targets[ it ];
1182
+ var links = this._getLinks( nodeId, targetId );
1183
+
1184
+ var linkIdx = links.findIndex( i => ( i.inputIdx == targetIndex && i.outputIdx == srcIndex ) );
1185
+ deleteElement( links[ linkIdx ].path.parentElement );
1186
+ links.splice( linkIdx, 1 );
1187
+
1188
+ // Remove a connection from the output connections
1189
+
1190
+ io.links[ targetIndex ].splice( it, 1 );
1191
+
1192
+ // Input has no longer any connected link
1193
+
1194
+ const targetDOM = this._getNodeDOMElement( targetId );
1195
+ const ios = targetDOM.querySelector( '.lexgraphnodeinputs' );
1196
+ const targetIO = ios.childNodes[ targetIndex ];
1197
+
1198
+ delete targetIO.links;
1199
+ delete targetIO.dataset[ 'active' ];
1200
+ }
1201
+ }
1202
+
1203
+ delete io.links;
1204
+ delete io.dataset[ 'active' ];
1205
+ }
1206
+ }
1207
+
1208
+ _processFocus( active ) {
1209
+
1210
+ this.isFocused = active;
1211
+ }
1212
+
1213
+ _processKeyDown( e ) {
1214
+
1215
+ // Prevent processing keys on inputs and others
1216
+ if( document.activeElement != this.root )
1217
+ return;
1218
+
1219
+ var key = e.key ?? e.detail.key;
1220
+
1221
+ switch( key ) {
1222
+ case 'Escape':
1223
+ this.unSelectAll();
1224
+ break;
1225
+ case 'Delete':
1226
+ case 'Backspace':
1227
+ e.preventDefault();
1228
+ this._deleteSelection( e );
1229
+ break;
1230
+ case 'g':
1231
+ if( e.ctrlKey )
1232
+ {
1233
+ e.preventDefault();
1234
+ this._createGroup();
1235
+ }
1236
+ break;
1237
+ case 'y':
1238
+ if( e.ctrlKey )
1239
+ {
1240
+ e.preventDefault();
1241
+ this._doRedo();
1242
+ }
1243
+ break;
1244
+ case 'z':
1245
+ if( e.ctrlKey )
1246
+ {
1247
+ e.preventDefault();
1248
+ this._doUndo();
1249
+ }
1250
+ break;
1251
+ }
1252
+
1253
+ this.keys[ key ] = true;
1254
+ }
1255
+
1256
+ _processKeyUp( e ) {
1257
+
1258
+ // Prevent processing keys on inputs and others
1259
+ if( document.activeElement != this.root )
1260
+ return;
1261
+
1262
+ var key = e.key ?? e.detail.key;
1263
+
1264
+ delete this.keys[ key ];
1265
+ }
1266
+
1267
+ _processMouse( e ) {
1268
+
1269
+ const rect = this.root.getBoundingClientRect();
1270
+
1271
+ this._mousePosition = new LX.vec2( e.clientX - rect.x , e.clientY - rect.y );
1272
+
1273
+ const snapPosition = new LX.vec2( this._mousePosition.x, this._mousePosition.y );
1274
+
1275
+ if( this._snapToGrid )
1276
+ {
1277
+ const snapSize = this._patternSize.x * this._snapValue * this._scale;
1278
+ snapPosition.x = Math.floor( snapPosition.x / snapSize ) * snapSize;
1279
+ snapPosition.y = Math.floor( snapPosition.y / snapSize ) * snapSize;
1280
+ this._snappedDeltaMousePosition = snapPosition.sub( this._lastSnappedMousePosition );
1281
+ }
1282
+
1283
+ this._deltaMousePosition = this._mousePosition.sub( this._lastMousePosition );
1284
+
1285
+ if( e.type == 'mousedown' )
1286
+ {
1287
+ this.lastMouseDown = LX.getTime();
1288
+
1289
+ this._processMouseDown( e );
1290
+ }
1291
+
1292
+ else if( e.type == 'mouseup' )
1293
+ {
1294
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1295
+ this._processClick( e );
1296
+ }
1297
+
1298
+ this._processMouseUp( e );
1299
+ }
1300
+
1301
+ else if( e.type == 'mousemove' )
1302
+ {
1303
+ this._processMouseMove( e );
1304
+ }
1305
+
1306
+ else if ( e.type == 'click' ) // trick
1307
+ {
1308
+ switch( e.detail )
1309
+ {
1310
+ case LX.MOUSE_DOUBLE_CLICK:
1311
+ break;
1312
+ case LX.MOUSE_TRIPLE_CLICK:
1313
+ break;
1314
+ }
1315
+ }
1316
+
1317
+ else if ( e.type == 'mousewheel' ) {
1318
+ e.preventDefault();
1319
+ this._processWheel( e );
1320
+ }
1321
+
1322
+ else if ( e.type == 'contextmenu' ) {
1323
+
1324
+ e.preventDefault();
1325
+
1326
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1327
+ this._processContextMenu( e );
1328
+ }
1329
+ }
1330
+
1331
+ else if ( e.type == 'mouseleave' ) {
1332
+
1333
+ if( this._generatingLink )
1334
+ {
1335
+ this._processMouseUp( e );
1336
+ }
1337
+ }
1338
+
1339
+ if( this._snapToGrid )
1340
+ {
1341
+ this._lastSnappedMousePosition = snapPosition;
1342
+ }
1343
+
1344
+ this._lastMousePosition = this._mousePosition;
1345
+ }
1346
+
1347
+ _processClick( e ) {
1348
+
1349
+ if( e.target.classList.contains( 'lexgraphnodes' ) || e.target.classList.contains( 'lexgraphgroup' ) )
1350
+ {
1351
+ this._processBackgroundClick( e );
1352
+ return;
1353
+ }
1354
+ }
1355
+
1356
+ _processBackgroundClick( e ) {
1357
+
1358
+ this.unSelectAll();
1359
+ }
1360
+
1361
+ _processMouseDown( e ) {
1362
+
1363
+ // Don't box select over a node..
1364
+ if( !e.target.classList.contains( 'lexgraphnode' ) && !e.target.classList.contains( 'lexgraphgroup' )
1365
+ && e.button == LX.MOUSE_LEFT_CLICK )
1366
+ {
1367
+ this._boxSelecting = this._mousePosition;
1368
+ this._boxSelectRemoving = e.altKey;
1369
+ }
1370
+ }
1371
+
1372
+ _processMouseUp( e ) {
1373
+
1374
+ // It the event reaches this, the link isn't valid..
1375
+ if( this._generatingLink )
1376
+ {
1377
+ deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
1378
+ delete this._generatingLink;
1379
+ }
1380
+
1381
+ else if( this._boxSelecting )
1382
+ {
1383
+ if( !e.ctrlKey && !e.altKey )
1384
+ this.unSelectAll();
1385
+
1386
+ this._selectNodesInBox( this._boxSelecting, this._mousePosition, e.altKey );
1387
+
1388
+ deleteElement( this._currentBoxSelectionSVG );
1389
+
1390
+ delete this._currentBoxSelectionSVG;
1391
+ delete this._boxSelecting;
1392
+ delete this._boxSelectRemoving;
1393
+ }
1394
+ }
1395
+
1396
+ _processMouseMove( e ) {
1397
+
1398
+ const rightPressed = ( e.which == 3 );
1399
+
1400
+ if( rightPressed )
1401
+ {
1402
+ this._patternPosition.add( this._deltaMousePosition.div( this._scale ), this._patternPosition );
1403
+
1404
+ this._updatePattern();
1405
+
1406
+ return;
1407
+ }
1408
+
1409
+ else if( this._generatingLink )
1410
+ {
1411
+ this._updatePreviewLink( e );
1412
+
1413
+ return;
1414
+ }
1415
+
1416
+ else if( this._boxSelecting )
1417
+ {
1418
+ this._drawBoxSelection( e );
1419
+
1420
+ return;
1421
+ }
1422
+ }
1423
+
1424
+ _processWheel( e ) {
1425
+
1426
+ if( this._boxSelecting )
1427
+ return;
1428
+
1429
+ // Compute zoom center in pattern space using current scale
1430
+
1431
+ const rect = this.root.getBoundingClientRect();
1432
+ const zoomCenter = this._mousePosition ?? new LX.vec2( rect.width * 0.5, rect.height * 0.5 );
1433
+
1434
+ const center = this._getPatternPosition( zoomCenter );
1435
+
1436
+ const delta = e.deltaY;
1437
+
1438
+ if( delta > 0.0 ) this._scale *= 0.9;
1439
+ else this._scale *= ( 1.0 / 0.9 );
1440
+
1441
+ this._scale = LX.UTILS.clamp( this._scale, GraphEditor.MIN_SCALE, GraphEditor.MAX_SCALE );
1442
+
1443
+ // Compute zoom center in pattern space using new scale
1444
+ // and get delta..
1445
+
1446
+ const newCenter = this._getPatternPosition( zoomCenter );
1447
+
1448
+ const deltaCenter = newCenter.sub( center );
1449
+
1450
+ this._patternPosition = this._patternPosition.add( deltaCenter );
1451
+
1452
+ this._updatePattern( GraphEditor.EVENT_MOUSEWHEEL );
1453
+ }
1454
+
1455
+ _processContextMenu( e ) {
1456
+
1457
+ LX.addContextMenu( "ADD NODE", e, m => {
1458
+
1459
+ for( let type in GraphEditor.NODE_TYPES )
1460
+ {
1461
+ m.add( type, () => {
1462
+
1463
+ const newNode = GraphEditor.addNode( type );
1464
+
1465
+ const dom = this._createNodeDOM( newNode );
1466
+
1467
+ if( this._snapToGrid )
1468
+ {
1469
+ dom.mustSnap = true;
1470
+ }
1471
+
1472
+ if( e )
1473
+ {
1474
+ const rect = this.root.getBoundingClientRect();
1475
+
1476
+ let position = new LX.vec2( e.clientX - rect.x, e.clientY - rect.y );
1477
+
1478
+ position = this._getPatternPosition( position );
1479
+
1480
+ this._translateNode( dom, position );
1481
+ }
1482
+
1483
+ this.graph.nodes.push( newNode );
1484
+
1485
+ } );
1486
+ }
1487
+ });
1488
+ }
1489
+
1490
+ /**
1491
+ * @method start
1492
+ */
1493
+
1494
+ start() {
1495
+
1496
+ this.mustStop = false;
1497
+
1498
+ this.propagateEventToAllNodes( 'onStart' );
1499
+
1500
+ requestAnimationFrame( this._frame.bind(this) );
1501
+ }
1502
+
1503
+ /**
1504
+ * @method stop
1505
+ */
1506
+
1507
+ stop() {
1508
+
1509
+ this.mustStop = true;
1510
+ this.state = GraphEditor.STOPPED;
1511
+
1512
+ this.propagateEventToAllNodes( 'onStop' );
1513
+ }
1514
+
1515
+ /**
1516
+ * @method _frame
1517
+ */
1518
+
1519
+ _frame() {
1520
+
1521
+ if( this.mustStop )
1522
+ {
1523
+ return;
1524
+ }
1525
+
1526
+ requestAnimationFrame( this._frame.bind(this) );
1527
+
1528
+ this._runStep();
1529
+ }
1530
+
1531
+ /**
1532
+ * @method _runStep
1533
+ */
1534
+
1535
+ _runStep() {
1536
+
1537
+ const main = this.main;
1538
+
1539
+ if( !main )
1540
+ return;
1541
+
1542
+ const visitedNodes = { };
1543
+
1544
+ this._executionNodes = [ ];
1545
+
1546
+ // Reser variables each step?
1547
+ this.variables = { };
1548
+
1549
+ const mainId = this.main.dataset[ 'id' ];
1550
+
1551
+ const addNode = ( id ) => {
1552
+
1553
+ if( visitedNodes[ id ] )
1554
+ return;
1555
+
1556
+ visitedNodes[ id ] = true;
1557
+
1558
+ for( let linkId in this.graph.links )
1559
+ {
1560
+ const idx = linkId.indexOf( '@' + id );
1561
+
1562
+ if( idx < 0 )
1563
+ continue;
1564
+
1565
+ const preNodeId = linkId.substring( 0, idx );
1566
+
1567
+ this._executionNodes.push( preNodeId );
1568
+
1569
+ addNode( preNodeId );
1570
+ }
1571
+ };
1572
+
1573
+ // TODO: Search "no output" nodes and add to the executable list (same as main)..
1574
+ // ...
1575
+
1576
+ this._executionNodes.push( mainId );
1577
+
1578
+ addNode( mainId );
1579
+
1580
+ for( var i = this._executionNodes.length - 1; i >= 0; --i )
1581
+ {
1582
+ const node = this.nodes[ this._executionNodes[ i ] ];
1583
+
1584
+ if( node.data.onBeforeStep )
1585
+ node.data.onBeforeStep();
1586
+
1587
+ node.data.execute();
1588
+
1589
+ if( node.data.onBeforeStep )
1590
+ node.data.onAfterStep();
1591
+ }
1592
+ }
1593
+
1594
+ _generatePattern() {
1595
+
1596
+ // Generate pattern
1597
+ {
1598
+ var pattern = document.createElementNS( 'http://www.w3.org/2000/svg', 'pattern' );
1599
+ pattern.setAttribute( 'id', 'pattern-0' );
1600
+ pattern.setAttribute( 'x', this._patternPosition.x );
1601
+ pattern.setAttribute( 'y', this._patternPosition.y );
1602
+ pattern.setAttribute( 'width', this._patternSize.x )
1603
+ pattern.setAttribute( 'height', this._patternSize.y );
1604
+ pattern.setAttribute( 'patternUnits', 'userSpaceOnUse' );
1605
+
1606
+ var circle = document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' );
1607
+ circle.setAttribute( 'cx', this._circlePatternSize );
1608
+ circle.setAttribute( 'cy', this._circlePatternSize );
1609
+ circle.setAttribute( 'r', this._circlePatternSize );
1610
+ circle.setAttribute( 'fill', this._circlePatternColor );
1611
+
1612
+ pattern.appendChild( circle );
1613
+ }
1614
+
1615
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
1616
+ svg.classList.add( "background-svg" );
1617
+ svg.style.width = "100%";
1618
+ svg.style.height = "100%";
1619
+
1620
+ svg.appendChild( pattern );
1621
+
1622
+ var rect = document.createElementNS( 'http://www.w3.org/2000/svg', 'rect' );
1623
+ rect.setAttribute( 'x', '0' );
1624
+ rect.setAttribute( 'y', '0' );
1625
+ rect.setAttribute( 'width', '100%' );
1626
+ rect.setAttribute( 'height', '100%' );
1627
+ rect.setAttribute( 'fill', 'url(#pattern-0)' );
1628
+
1629
+ svg.appendChild( rect );
1630
+
1631
+ this._background = svg;
1632
+
1633
+ this.root.appendChild( this._background );
1634
+ }
1635
+
1636
+ _updatePattern() {
1637
+
1638
+ if( !this._background )
1639
+ return;
1640
+
1641
+ const patternSize = this._patternSize.mul( this._scale );
1642
+ const circlePatternSize = this._circlePatternSize * this._scale;
1643
+ const patternPosition = this._patternPosition.mul( this._scale );
1644
+
1645
+ let pattern = this._background.querySelector( 'pattern' );
1646
+ pattern.setAttribute( 'x', patternPosition.x );
1647
+ pattern.setAttribute( 'y', patternPosition.y );
1648
+ pattern.setAttribute( 'width', patternSize.x )
1649
+ pattern.setAttribute( 'height', patternSize.y );
1650
+
1651
+ var circle = this._background.querySelector( 'circle' );
1652
+ circle.setAttribute( 'cx', circlePatternSize );
1653
+ circle.setAttribute( 'cy', circlePatternSize );
1654
+ circle.setAttribute( 'r', circlePatternSize );
1655
+
1656
+ // Nodes
1657
+
1658
+ const w = this._domNodes.offsetWidth * 0.5;
1659
+ const h = this._domNodes.offsetHeight * 0.5;
1660
+
1661
+ const dw = w - w * this._scale;
1662
+ const dh = h - h * this._scale;
1663
+
1664
+ this._domNodes.style.transform = `
1665
+ translate(` + ( patternPosition.x - dw ) + `px, ` + ( patternPosition.y - dh ) + `px)
1666
+ scale(` + this._scale + `)
1667
+ `;
1668
+ this._domLinks.style.transform = this._domNodes.style.transform;
1669
+ }
1670
+
1671
+ _getPatternPosition( renderPosition ) {
1672
+
1673
+ return renderPosition.div( this._scale ).sub( this._patternPosition );
1674
+ }
1675
+
1676
+ _getRenderPosition( patternPosition ) {
1677
+
1678
+ return patternPosition.add( this._patternPosition ).mul( this._scale );
1679
+ }
1680
+
1681
+ _onLink( e ) {
1682
+
1683
+ const linkData = this._generatingLink;
1684
+ const ioType = e.target.classList.contains( 'input' ) ? GraphEditor.NODE_IO_INPUT : GraphEditor.NODE_IO_OUTPUT;
1685
+
1686
+ // Discard same IO type
1687
+ if( linkData.ioType == ioType )
1688
+ {
1689
+ console.warn( `Can't link same type of data` );
1690
+ return;
1691
+ }
1692
+
1693
+ // Info about src node
1694
+ const src_nodeContainer = linkData.domEl;
1695
+ const src_nodeId = src_nodeContainer.dataset[ 'id' ];
1696
+ const src_node = this.nodes[ src_nodeId ].data;
1697
+ const src_ioIndex = this._generatingLink.index
1698
+
1699
+ // Info about dst node
1700
+ const dst_nodeContainer = e.target.offsetParent;
1701
+ const dst_nodeId = dst_nodeContainer.dataset[ 'id' ];
1702
+ const dst_node = this.nodes[ dst_nodeId ].data;
1703
+ const dst_ioIndex = parseInt( e.target.parentElement.dataset[ 'index' ] );
1704
+
1705
+ // Discard different types
1706
+ const srcIsInput = ( linkData.ioType == GraphEditor.NODE_IO_INPUT );
1707
+ const src_ios = src_node[ srcIsInput ? 'inputs' : 'outputs' ];
1708
+ const src_ioType = src_ios[ src_ioIndex ].type;
1709
+
1710
+ const dst_ios = dst_node[ ioType == GraphEditor.NODE_IO_INPUT ? 'inputs' : 'outputs' ];
1711
+ const dst_ioType = dst_ios[ dst_ioIndex ].type;
1712
+
1713
+ if( src_ioType != dst_ioType && src_ioType != "any" && dst_ioType != "any" )
1714
+ {
1715
+ // Different types, but it might be possible to cast types
1716
+
1717
+ const inputType = srcIsInput ? src_ioType : dst_ioType;
1718
+ const outputType = srcIsInput ? dst_ioType : src_ioType;
1719
+
1720
+ if( !this.supportedCastTypes[ outputType + '@' + inputType ] )
1721
+ {
1722
+ console.warn( `Can't link ${ src_ioType } to ${ dst_ioType }.` );
1723
+ return;
1724
+ }
1725
+ }
1726
+
1727
+ // Check if target it's an active input and remove the old link
1728
+
1729
+ if( ioType == GraphEditor.NODE_IO_INPUT && e.target.parentElement.dataset[ 'active' ] )
1730
+ {
1731
+ this._deleteLinks( dst_nodeId, e.target.parentElement );
1732
+ }
1733
+
1734
+ // Check if source it's an active input and remove the old link
1735
+
1736
+ else if( linkData.ioType == GraphEditor.NODE_IO_INPUT && linkData.io.dataset[ 'active' ] )
1737
+ {
1738
+ this._deleteLinks( src_nodeId, linkData.io );
1739
+ }
1740
+
1741
+ // Mark as active
1742
+
1743
+ linkData.io.dataset[ 'active' ] = true;
1744
+ e.target.parentElement.dataset[ 'active' ] = true;
1745
+
1746
+ // Store the end io..
1747
+
1748
+ var srcDom = linkData.io;
1749
+ srcDom.links = srcDom.links ?? [ ];
1750
+ srcDom.links[ dst_ioIndex ] = srcDom.links[ dst_ioIndex ] ?? [ ];
1751
+ srcDom.links[ dst_ioIndex ].push( dst_nodeId );
1752
+
1753
+ var dstDom = e.target.parentElement;
1754
+ dstDom.links = dstDom.links ?? [ ];
1755
+ dstDom.links[ src_ioIndex ] = dstDom.links[ src_ioIndex ] ?? [ ];
1756
+ dstDom.links[ src_ioIndex ].push( src_nodeId );
1757
+
1758
+ // Call this using the io target to set the connection to the center of the input DOM element..
1759
+
1760
+ let path = this._updatePreviewLink( null, e.target.parentElement );
1761
+
1762
+ // Store link
1763
+
1764
+ const pathId = ( srcIsInput ? dst_nodeId : src_nodeId ) + '@' + ( srcIsInput ? src_nodeId : dst_nodeId );
1765
+
1766
+ if( !this.graph.links[ pathId ] ) this.graph.links[ pathId ] = [];
1767
+
1768
+ this.graph.links[ pathId ].push( {
1769
+ path: path,
1770
+ inputNode: srcIsInput ? src_nodeId : dst_nodeId,
1771
+ inputIdx: srcIsInput ? src_ioIndex : dst_ioIndex,
1772
+ inputType: srcIsInput ? src_ioType : dst_ioType,
1773
+ outputNode: srcIsInput ? dst_nodeId : src_nodeId,
1774
+ outputIdx: srcIsInput ? dst_ioIndex : src_ioIndex,
1775
+ outputType: srcIsInput ? dst_ioType : src_ioType,
1776
+ } );
1777
+
1778
+ path.dataset[ 'id' ] = pathId;
1779
+
1780
+ // Successful link..
1781
+ return true;
1782
+ }
1783
+
1784
+ _updatePreviewLink( e, endIO ) {
1785
+
1786
+ var path = this._generatingLink.path;
1787
+
1788
+ if( !path )
1789
+ {
1790
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
1791
+ svg.classList.add( "link-svg" );
1792
+ svg.style.width = "100%";
1793
+ svg.style.height = "100%";
1794
+ this._domLinks.appendChild( svg );
1795
+
1796
+ path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
1797
+ path.setAttribute( 'fill', 'none' );
1798
+ svg.appendChild( path );
1799
+ this._generatingLink.path = path;
1800
+ }
1801
+
1802
+ // Generate bezier curve
1803
+
1804
+ const index = this._generatingLink.index;
1805
+ const type = this._generatingLink.ioType;
1806
+ const domEl = this._generatingLink.domEl;
1807
+
1808
+ const offsetX = this.root.getBoundingClientRect().x;
1809
+ const offsetY = this.root.getBoundingClientRect().y;
1810
+
1811
+ const ios = domEl.querySelector( type == GraphEditor.NODE_IO_INPUT ? '.lexgraphnodeinputs' : '.lexgraphnodeoutputs' );
1812
+ const ioEl = ios.childNodes[ index ].querySelector( '.io__type' );
1813
+ const startRect = ioEl.getBoundingClientRect();
1814
+
1815
+ let startPos = new LX.vec2( startRect.x - offsetX, startRect.y - offsetY );
1816
+ let endPos = null;
1817
+
1818
+ if( e )
1819
+ {
1820
+ endPos = new LX.vec2( e.offsetX, e.offsetY );
1821
+
1822
+ // Add node position, since I can't get the correct position directly from the event..
1823
+ if( e.target.hasClass( [ 'lexgraphnode', 'lexgraphgroup' ] ) )
1824
+ {
1825
+ console.log( this._getNodePosition( e.target ) );
1826
+ endPos.add( this._getNodePosition( e.target ), endPos );
1827
+ endPos.add( new LX.vec2( 3, 3 ), endPos );
1828
+ }
1829
+ else if( e.target.hasClass( [ 'io__type', 'lexgraphgroupresizer' ] ) )
1830
+ {
1831
+ var parent = e.target.offsetParent;
1832
+ // Add parent offset
1833
+ endPos.add( this._getNodePosition( parent ), endPos );
1834
+ // Add own offset
1835
+ endPos.add( new LX.vec2( e.target.offsetLeft, e.target.offsetTop ), endPos );
1836
+ endPos.add( new LX.vec2( 3, 3 ), endPos );
1837
+ }
1838
+
1839
+ endPos = this._getRenderPosition( endPos );
1840
+ }
1841
+ else
1842
+ {
1843
+ const ioRect = endIO.querySelector( '.io__type' ).getBoundingClientRect();
1844
+ endPos = new LX.vec2( ioRect.x - offsetX, ioRect.y - offsetY );
1845
+ }
1846
+
1847
+ if( type == GraphEditor.NODE_IO_INPUT )
1848
+ {
1849
+ var tmp = endPos;
1850
+ endPos = startPos;
1851
+ startPos = tmp;
1852
+ }
1853
+
1854
+ const color = getComputedStyle( ioEl ).backgroundColor;
1855
+ this._createLinkPath( path, startPos, endPos, color, !!e );
1856
+
1857
+ return path;
1858
+ }
1859
+
1860
+ _createLink( link ) {
1861
+
1862
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
1863
+ svg.classList.add( "link-svg" );
1864
+ svg.style.width = "100%";
1865
+ svg.style.height = "100%";
1866
+ this._domLinks.appendChild( svg );
1867
+
1868
+ var path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
1869
+ path.setAttribute( 'fill', 'none' );
1870
+ svg.appendChild( path );
1871
+
1872
+ const inputNodeDom = this._getNodeDOMElement( link.inputNode );
1873
+ const outputNodeDom = this._getNodeDOMElement( link.outputNode );
1874
+
1875
+ // Start pos
1876
+
1877
+ const offsetX = this.root.getBoundingClientRect().x;
1878
+ const offsetY = this.root.getBoundingClientRect().y;
1879
+
1880
+ const outputs = outputNodeDom.querySelector( '.lexgraphnodeoutputs' );
1881
+ const io0 = outputs.childNodes[ link.outputIdx ];
1882
+ const startRect = io0.querySelector( '.io__type' ).getBoundingClientRect();
1883
+
1884
+ let startPos = new LX.vec2( startRect.x - offsetX, startRect.y - offsetY + 6 );
1885
+
1886
+ // End pos
1887
+
1888
+ const inputs = inputNodeDom.querySelector( '.lexgraphnodeinputs' );
1889
+ const io1 = inputs.childNodes[ link.inputIdx ];
1890
+ const endRect = io1.querySelector( '.io__type' ).getBoundingClientRect();
1891
+
1892
+ let endPos = new LX.vec2( endRect.x - offsetX, endRect.y - offsetY + 6 );
1893
+
1894
+ // Generate bezier curve
1895
+
1896
+ const color = getComputedStyle( io1.querySelector( '.io__type' ) ).backgroundColor;
1897
+ this._createLinkPath( path, startPos, endPos, color );
1898
+
1899
+ link.path = path;
1900
+
1901
+ // Store data in each IO
1902
+
1903
+ io0.links = [ ];
1904
+ io0.links[ link.inputIdx ] = io0.links[ link.inputIdx ] ?? [ ];
1905
+ io0.links[ link.inputIdx ].push( link.inputNode );
1906
+
1907
+ io1.links = [ ];
1908
+ io1.links[ link.outputIdx ] = io1.links[ link.outputIdx ] ?? [ ];
1909
+ io1.links[ link.outputIdx ].push( link.outputNode );
1910
+ }
1911
+
1912
+ _createLinkPath( path, startPos, endPos, color, exactEnd ) {
1913
+
1914
+ const dist = 6 * this._scale;
1915
+ startPos.add( new LX.vec2( dist, dist ), startPos );
1916
+
1917
+ if( !exactEnd )
1918
+ {
1919
+ endPos.add( new LX.vec2( dist, dist ), endPos );
1920
+ }
1921
+
1922
+ startPos = this._getPatternPosition( startPos );
1923
+ endPos = this._getPatternPosition( endPos );
1924
+
1925
+ const distanceX = LX.UTILS.clamp( Math.abs( startPos.x - endPos.x ), 0.0, 256.0 );
1926
+ const cPDistance = 128.0 * Math.pow( distanceX / 256.0, 0.5 );
1927
+
1928
+ let cPoint1 = startPos.add( new LX.vec2( cPDistance, 0 ) );
1929
+ let cPoint2 = endPos.sub( new LX.vec2( cPDistance, 0 ) );
1930
+
1931
+ path.setAttribute( 'd', `
1932
+ M ${ startPos.x },${ startPos.y }
1933
+ C ${ cPoint1.x },${ cPoint1.y } ${ cPoint2.x },${ cPoint2.y } ${ endPos.x },${ endPos.y }
1934
+ ` );
1935
+
1936
+ path.setAttribute( 'stroke', color );
1937
+ }
1938
+
1939
+ _updateNodeLinks( nodeId ) {
1940
+
1941
+ var node = this.nodes[ nodeId ] ? this.nodes[ nodeId ].data : null;
1942
+
1943
+ if( !node ) {
1944
+ console.warn( `Can't finde node [${ nodeId }]` );
1945
+ return;
1946
+ }
1947
+
1948
+ const nodeDOM = this._getNodeDOMElement( nodeId );
1949
+
1950
+ // Update input links
1951
+
1952
+ for( let input of nodeDOM.querySelectorAll( '.ioinput' ) )
1953
+ {
1954
+ if( !input.links )
1955
+ continue;
1956
+
1957
+ // Get first and only target output..
1958
+ const targets = input.links.filter( v => v !== undefined )[ 0 ];
1959
+ const targetNodeId = targets[ 0 ];
1960
+
1961
+ // It has been deleted..
1962
+ if( !targetNodeId )
1963
+ continue;
1964
+
1965
+ const ioIndex = parseInt( input.dataset[ 'index' ] );
1966
+
1967
+ var links = this._getLinks( targetNodeId, nodeId );
1968
+
1969
+ // Inputs only have 1 possible output connected
1970
+ var link = links.find( i => ( i.inputIdx == ioIndex ) );
1971
+
1972
+ this._generatingLink = {
1973
+ index: ioIndex,
1974
+ io: input,
1975
+ ioType: GraphEditor.NODE_IO_INPUT,
1976
+ domEl: nodeDOM,
1977
+ path: link.path
1978
+ };
1979
+
1980
+ // Get end io
1981
+
1982
+ const outputNode = this._getNodeDOMElement( targetNodeId );
1983
+ const io = outputNode.querySelector( '.lexgraphnodeoutputs' ).childNodes[ link.outputIdx ]
1984
+
1985
+ this._updatePreviewLink( null, io );
1986
+ }
1987
+
1988
+ // Update output links
1989
+
1990
+
1991
+ for( let output of nodeDOM.querySelectorAll( '.iooutput' ) )
1992
+ {
1993
+ if( !output.links )
1994
+ continue;
1995
+
1996
+ const srcIndex = parseInt( output.dataset[ 'index' ] );
1997
+
1998
+ for( let targetIndex = 0; targetIndex < output.links.length; ++targetIndex )
1999
+ {
2000
+ const targets = output.links[ targetIndex ];
2001
+
2002
+ if( !targets )
2003
+ continue;
2004
+
2005
+ for( let targetId of targets )
2006
+ {
2007
+ var links = this._getLinks( nodeId, targetId );
2008
+ var link = links.find( i => ( i.inputIdx == targetIndex && i.outputIdx == srcIndex ) );
2009
+
2010
+ // Outputs can have different inputs connected
2011
+ this._generatingLink = {
2012
+ index: link.outputIdx,
2013
+ io: output,
2014
+ ioType: GraphEditor.NODE_IO_OUTPUT,
2015
+ domEl: nodeDOM,
2016
+ path: link.path
2017
+ };
2018
+
2019
+ // Get end io
2020
+
2021
+ const inputNode = this._getNodeDOMElement( targetId );
2022
+ const io = inputNode.querySelector( '.lexgraphnodeinputs' ).childNodes[ link.inputIdx ]
2023
+
2024
+ this._updatePreviewLink( null, io );
2025
+ }
2026
+ }
2027
+ }
2028
+
2029
+ delete this._generatingLink;
2030
+ }
2031
+
2032
+ _drawBoxSelection( e ) {
2033
+
2034
+ var svg = this._currentBoxSelectionSVG;
2035
+
2036
+ if( !svg )
2037
+ {
2038
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
2039
+ svg.classList.add( "box-selection-svg" );
2040
+ if( this._boxSelectRemoving )
2041
+ svg.classList.add( "removing" );
2042
+ svg.style.width = "100%";
2043
+ svg.style.height = "100%";
2044
+ this._domLinks.appendChild( svg );
2045
+ this._currentBoxSelectionSVG = svg;
2046
+ }
2047
+
2048
+ // Generate box
2049
+
2050
+ let startPos = this._getPatternPosition( this._boxSelecting );
2051
+ let size = this._getPatternPosition( this._mousePosition ).sub( startPos );
2052
+
2053
+ if( size.x < 0 ) startPos.x += size.x;
2054
+ if( size.y < 0 ) startPos.y += size.y;
2055
+
2056
+ size = size.abs();
2057
+
2058
+ svg.innerHTML = `<rect
2059
+ x="${ startPos.x }" y="${ startPos.y }"
2060
+ rx="${ 6 }" ry="${ 6 }"
2061
+ width="${ size.x }" height="${ size.y }"
2062
+ "/>`;
2063
+ }
2064
+
2065
+ // TODO: Return the ones in the viewport
2066
+ _getVisibleNodes() {
2067
+
2068
+ if( !this.graph )
2069
+ {
2070
+ console.warn( "No graph set" );
2071
+ return [];
2072
+ }
2073
+
2074
+ return this.graph.nodes;
2075
+ }
2076
+
2077
+ _selectNodesInBox( lt, rb, remove ) {
2078
+
2079
+ lt = this._getPatternPosition( lt );
2080
+ rb = this._getPatternPosition( rb );
2081
+
2082
+ let size = rb.sub( lt );
2083
+
2084
+ if( size.x < 0 )
2085
+ {
2086
+ var tmp = lt.x;
2087
+ lt.x = rb.x;
2088
+ rb.x = tmp;
2089
+ }
2090
+
2091
+ if( size.y < 0 )
2092
+ {
2093
+ var tmp = lt.y;
2094
+ lt.y = rb.y;
2095
+ rb.y = tmp;
2096
+ }
2097
+
2098
+ for( let nodeEl of this._getAllDOMNodes() )
2099
+ {
2100
+ let pos = this._getNodePosition( nodeEl );
2101
+ let size = new LX.vec2( nodeEl.offsetWidth, nodeEl.offsetHeight );
2102
+
2103
+ if( ( !( pos.x < lt.x && ( pos.x + size.x ) < lt.x ) && !( pos.x > rb.x && ( pos.x + size.x ) > rb.x ) ) &&
2104
+ ( !( pos.y < lt.y && ( pos.y + size.y ) < lt.y ) && !( pos.y > rb.y && ( pos.y + size.y ) > rb.y ) ) )
2105
+ {
2106
+ if( remove )
2107
+ this._unSelectNode( nodeEl );
2108
+ else
2109
+ this._selectNode( nodeEl, true, false );
2110
+ }
2111
+ }
2112
+ }
2113
+
2114
+ _deleteSelection( e ) {
2115
+
2116
+ const lastNodeCount = this._domNodes.childElementCount;
2117
+
2118
+ for( let nodeId of this.selectedNodes )
2119
+ {
2120
+ this._deleteNode( nodeId );
2121
+ }
2122
+
2123
+ this.selectedNodes.length = 0;
2124
+
2125
+ // We delete something, so add undo step..
2126
+
2127
+ if( this._domNodes.childElementCount != lastNodeCount )
2128
+ {
2129
+ this._addUndoStep();
2130
+ }
2131
+
2132
+ }
2133
+
2134
+ _getBoundingFromGroup( groupDOM ) {
2135
+
2136
+ const x = parseFloat( groupDOM.style.left );
2137
+ const y = parseFloat( groupDOM.style.top );
2138
+
2139
+ return new BoundingBox( new LX.vec2( x, y ), new LX.vec2( groupDOM.offsetWidth - 2, groupDOM.offsetHeight - 2 ) );
2140
+ }
2141
+
2142
+ _getBoundingFromNodes( nodeIds ) {
2143
+
2144
+ let group_bb = null;
2145
+
2146
+ for( let nodeId of nodeIds )
2147
+ {
2148
+ const dom = this._getNodeDOMElement( nodeId );
2149
+
2150
+ const x = parseFloat( dom.style.left );
2151
+ const y = parseFloat( dom.style.top );
2152
+ const node_bb = new BoundingBox( new LX.vec2( x, y ), new LX.vec2( dom.offsetWidth - 6, dom.offsetHeight - 6 ) );
2153
+
2154
+ if( group_bb )
2155
+ {
2156
+ group_bb.merge( node_bb );
2157
+ }
2158
+ else
2159
+ {
2160
+ group_bb = node_bb;
2161
+ }
2162
+ }
2163
+
2164
+ // Add padding
2165
+
2166
+ const groupContentPadding = 8;
2167
+
2168
+ group_bb.origin.sub( new LX.vec2( groupContentPadding ), group_bb.origin );
2169
+ group_bb.origin.sub( new LX.vec2( groupContentPadding ), group_bb.origin );
2170
+
2171
+ group_bb.size.add( new LX.vec2( groupContentPadding * 2.0 ), group_bb.size );
2172
+ group_bb.size.add( new LX.vec2( groupContentPadding * 2.0 ), group_bb.size );
2173
+
2174
+ return group_bb;
2175
+ }
2176
+
2177
+ /**
2178
+ * @method _createGroup
2179
+ * @description Creates a node group from the bounding box of the selected nodes
2180
+ * @returns JSON data from the serialized graph
2181
+ */
2182
+
2183
+ _createGroup() {
2184
+
2185
+ const group_bb = this._getBoundingFromNodes( this.selectedNodes );
2186
+
2187
+ let groupDOM = document.createElement( 'div' );
2188
+ groupDOM.classList.add( 'lexgraphgroup' );
2189
+ groupDOM.style.left = group_bb.origin.x + "px";
2190
+ groupDOM.style.top = group_bb.origin.y + "px";
2191
+ groupDOM.style.width = group_bb.size.x + "px";
2192
+ groupDOM.style.height = group_bb.size.y + "px";
2193
+
2194
+ let groupResizer = document.createElement( 'div' );
2195
+ groupResizer.classList.add( 'lexgraphgroupresizer' );
2196
+
2197
+ groupResizer.addEventListener( 'mousedown', inner_mousedown );
2198
+
2199
+ var that = this;
2200
+ var lastPos = [0,0];
2201
+
2202
+ function inner_mousedown( e )
2203
+ {
2204
+ var doc = that.root.ownerDocument;
2205
+ doc.addEventListener( 'mousemove', inner_mousemove );
2206
+ doc.addEventListener( 'mouseup', inner_mouseup );
2207
+ lastPos[0] = e.x;
2208
+ lastPos[1] = e.y;
2209
+ e.stopPropagation();
2210
+ e.preventDefault();
2211
+ document.body.classList.add( 'nocursor' );
2212
+ groupResizer.classList.add( 'nocursor' );
2213
+ }
2214
+
2215
+ function inner_mousemove( e )
2216
+ {
2217
+ let dt = new LX.vec2( lastPos[0] - e.x, lastPos[1] - e.y );
2218
+ dt.div( that._scale, dt);
2219
+
2220
+ groupDOM.style.width = ( parseFloat( groupDOM.style.width ) - dt.x ) + "px";
2221
+ groupDOM.style.height = ( parseFloat( groupDOM.style.height ) - dt.y ) + "px";
2222
+
2223
+ lastPos[0] = e.x;
2224
+ lastPos[1] = e.y;
2225
+
2226
+ e.stopPropagation();
2227
+ e.preventDefault();
2228
+ }
2229
+
2230
+ function inner_mouseup( e )
2231
+ {
2232
+ var doc = that.root.ownerDocument;
2233
+ doc.removeEventListener( 'mousemove', inner_mousemove );
2234
+ doc.removeEventListener( 'mouseup', inner_mouseup );
2235
+ document.body.classList.remove( 'nocursor' );
2236
+ groupResizer.classList.remove( 'nocursor' );
2237
+ }
2238
+
2239
+ let groupTitle = document.createElement( 'input' );
2240
+ let defaultName = `Group ${ GraphEditor.LAST_GROUP_ID }`;
2241
+ groupTitle.value = defaultName;
2242
+ groupTitle.classList.add( 'lexgraphgrouptitle' );
2243
+ groupTitle.disabled = true;
2244
+
2245
+ // Dbl click to rename
2246
+
2247
+ groupTitle.addEventListener( 'mousedown', e => {
2248
+ e.stopPropagation();
2249
+ e.stopImmediatePropagation();
2250
+ } );
2251
+
2252
+ groupTitle.addEventListener( 'focusout', e => {
2253
+ groupTitle.disabled = true;
2254
+ if( !groupTitle.value.length )
2255
+ groupTitle.value = defaultName;
2256
+ } );
2257
+
2258
+ groupTitle.addEventListener( 'keyup', e => {
2259
+ if( e.key == 'Enter' ) {
2260
+ groupTitle.blur();
2261
+ }
2262
+ else if( e.key == 'Escape' ) {
2263
+ groupTitle.value = "";
2264
+ groupTitle.blur();
2265
+ }
2266
+ });
2267
+
2268
+ groupDOM.addEventListener( 'dblclick', e => {
2269
+ // Only for left click..
2270
+ if( e.button != LX.MOUSE_LEFT_CLICK )
2271
+ return;
2272
+ groupTitle.disabled = false;
2273
+ groupTitle.focus();
2274
+ } );
2275
+
2276
+ groupDOM.appendChild( groupResizer );
2277
+ groupDOM.appendChild( groupTitle );
2278
+
2279
+ this._domNodes.prepend( groupDOM );
2280
+
2281
+ // Move group!!
2282
+
2283
+ LX.makeDraggable( groupDOM, {
2284
+ onMove: this._onMoveGroup.bind( this ),
2285
+ onDragStart: this._onDragGroup.bind( this )
2286
+ } );
2287
+
2288
+ GraphEditor.LAST_GROUP_ID++;
2289
+ }
2290
+
2291
+ _addUndoStep( deleteRedo = true ) {
2292
+
2293
+ if( deleteRedo )
2294
+ {
2295
+ // Remove all redo steps
2296
+ this._redoSteps.length = 0;
2297
+ }
2298
+
2299
+ this._undoSteps.push( {
2300
+ // TODO: Add graph state
2301
+ } );
2302
+ }
2303
+
2304
+ _doUndo() {
2305
+
2306
+ if( !this._undoSteps.length )
2307
+ return;
2308
+
2309
+ this._addRedoStep();
2310
+
2311
+ // Extract info from the last state
2312
+ const step = this._undoSteps.pop();
2313
+
2314
+ // Set old state
2315
+ // TODO
2316
+
2317
+ console.log( "Undo!!" );
2318
+ }
2319
+
2320
+ _addRedoStep() {
2321
+
2322
+ this._redoSteps.push( {
2323
+ // TODO: Add graph state
2324
+ } );
2325
+ }
2326
+
2327
+ _doRedo() {
2328
+
2329
+ if( !this._redoSteps.length )
2330
+ return;
2331
+
2332
+ this._addUndoStep( false );
2333
+
2334
+ // Extract info from the next saved code state
2335
+ const step = this._redoSteps.pop();
2336
+
2337
+ // Set old state
2338
+ // TODO
2339
+
2340
+ console.log( "Redo!!" );
2341
+ }
2342
+
2343
+ _togglePropertiesDialog( force ) {
2344
+
2345
+ this.propertiesDialog.root.classList.toggle( 'opened', force );
2346
+
2347
+ if( !force )
2348
+ {
2349
+ this.propertiesDialog.panel.clear();
2350
+ }
2351
+ }
2352
+
2353
+ _setSnappingValue( value ) {
2354
+
2355
+ this._snapValue = value;
2356
+ }
2357
+
2358
+ _toggleSnapping() {
2359
+
2360
+ this._snapToGrid = !this._snapToGrid;
2361
+
2362
+ // Trigger position snapping for each node if needed
2363
+
2364
+ if( this._snapToGrid )
2365
+ {
2366
+ for( let nodeDom of this._getAllDOMNodes( true ) )
2367
+ {
2368
+ nodeDom.mustSnap = true;
2369
+ }
2370
+ }
2371
+ }
2372
+
2373
+ _toggleSideBar() {
2374
+
2375
+ this._sidebar.classList.toggle( 'hidden' );
2376
+ }
2377
+ }
2378
+
2379
+ LX.GraphEditor = GraphEditor;
2380
+
2381
+
2382
+ /**
2383
+ * @class Graph
2384
+ */
2385
+
2386
+ class Graph {
2387
+
2388
+ /**
2389
+ * @param {*} options
2390
+ *
2391
+ */
2392
+
2393
+ constructor( name, options = {} ) {
2394
+
2395
+ this.name = name ?? "Unnamed Graph";
2396
+
2397
+ // Nodes
2398
+
2399
+ this.nodes = [ ];
2400
+
2401
+ this.links = { };
2402
+
2403
+ // const mainNode = GraphEditor.addNode( 'system/Main' );
2404
+ // mainNode.position = new LX.vec2( 650, 400 );
2405
+
2406
+ // const addNode = GraphEditor.addNode( 'math/Add' );
2407
+ // addNode.position = new LX.vec2( 425, 250 );
2408
+
2409
+ // const floatNode = GraphEditor.addNode( 'inputs/Float' );
2410
+ // floatNode.position = new LX.vec2( 200, 200 );
2411
+
2412
+ // const stringNode = GraphEditor.addNode( 'inputs/String' );
2413
+ // stringNode.position = new LX.vec2( 400, 50 );
2414
+
2415
+ // const setVarNode = GraphEditor.addNode( 'variables/SetVariable' );
2416
+ // setVarNode.position = new LX.vec2( 650, 50 );
2417
+
2418
+ // // const multNode = new GraphNode( GraphEditor.DEFAULT_NODES[ 'Multiply' ] );
2419
+ // // multNode.position = new LX.vec2( 200, 400 );
2420
+
2421
+ // const keydownNode = GraphEditor.addNode( 'events/KeyDown' );
2422
+ // keydownNode.position = new LX.vec2( 600, 200 );
2423
+
2424
+ // const orNode = GraphEditor.addNode( 'logic/Or' );
2425
+ // orNode.position = new LX.vec2( 435, 435 );
2426
+
2427
+ // const equalNode = GraphEditor.addNode( 'logic/Select' );
2428
+ // equalNode.position = new LX.vec2( 135, 400 );
2429
+
2430
+ // this.nodes = [
2431
+ // mainNode,
2432
+ // addNode,
2433
+ // floatNode,
2434
+ // setVarNode,
2435
+ // stringNode,
2436
+ // keydownNode,
2437
+ // orNode,
2438
+ // equalNode,
2439
+ // // multNode
2440
+ // ];
2441
+ }
2442
+
2443
+ configure( o ) {
2444
+
2445
+ this.nodes.length = 0;
2446
+
2447
+ for( let node of o.nodes )
2448
+ {
2449
+ const newNode = GraphEditor.addNode( node.type );
2450
+
2451
+ newNode.id = node.id;
2452
+ newNode.title = node.title;
2453
+ newNode.color = node.color;
2454
+ newNode.position = new LX.vec2( node.position.x, node.position.y );
2455
+ newNode.type = node.type;
2456
+ // newNode.inputs = node.inputs;
2457
+ // newNode.outputs = node.outputs;
2458
+ // newNode.properties = node.properties;
2459
+
2460
+ this.nodes.push( newNode );
2461
+ }
2462
+
2463
+ this.links = o.links;
2464
+
2465
+ // editor options?
2466
+
2467
+ // zoom/translation ??
2468
+ }
2469
+
2470
+ /**
2471
+ * @method export
2472
+ * @param {Boolean} prettify
2473
+ * @returns JSON data from the serialized graph
2474
+ */
2475
+
2476
+ export( prettify = true ) {
2477
+
2478
+ var o = { };
2479
+ o.nodes = [ ];
2480
+ o.links = { };
2481
+
2482
+ for( let node of this.nodes )
2483
+ {
2484
+ o.nodes.push( node.serialize() );
2485
+ }
2486
+
2487
+ for( let linkId in this.links )
2488
+ {
2489
+ const ioLinks = LX.deepCopy( this.links[ linkId ] );
2490
+ ioLinks.forEach( v => delete v.path );
2491
+ o.links[ linkId ] = ioLinks;
2492
+ }
2493
+
2494
+ // editor options?
2495
+
2496
+ // zoom/translation ??
2497
+
2498
+ try
2499
+ {
2500
+ o = JSON.stringify( o, null, prettify ? 2 : null );
2501
+ }
2502
+ catch( e )
2503
+ {
2504
+ o = null;
2505
+ console.error( `Can't export GraphNode [${ this.title }] of type [${ this.type }].` );
2506
+ }
2507
+
2508
+ LX.downloadFile( this.name + ".json", o );
2509
+
2510
+ return o;
2511
+ }
2512
+ }
2513
+
2514
+ LX.Graph = Graph;
2515
+
2516
+ /**
2517
+ * @class GraphNode
2518
+ */
2519
+
2520
+ class GraphNode {
2521
+
2522
+ constructor() {
2523
+
2524
+ this.inputs = [ ];
2525
+ this.outputs = [ ];
2526
+ this.properties = [ ];
2527
+ }
2528
+
2529
+ _hasOutputsConnected() {
2530
+
2531
+ return true;
2532
+ }
2533
+
2534
+ execute() {
2535
+
2536
+ if( !this._hasOutputsConnected() )
2537
+ return;
2538
+
2539
+ if( this.onExecute )
2540
+ {
2541
+ this.onExecute();
2542
+ }
2543
+ }
2544
+
2545
+ addInput( name, type ) {
2546
+
2547
+ this.inputs.push( { name: name, type: type } );
2548
+ }
2549
+
2550
+ addOutput( name, type ) {
2551
+
2552
+ this.outputs.push( { name: name, type: type } );
2553
+ }
2554
+
2555
+ addProperty( name, type, value, selectOptions ) {
2556
+
2557
+ this.properties.push( { name: name, type: type, value: value, options: selectOptions } );
2558
+ }
2559
+
2560
+ getInput( index ) {
2561
+
2562
+ if( !this.inputs || !this.inputs.length || !this.inputs[ index ] )
2563
+ return;
2564
+
2565
+ // Get data from link
2566
+
2567
+ for( let linkId in this.editor.graph.links )
2568
+ {
2569
+ const idx = linkId.indexOf( '@' + this.id );
2570
+
2571
+ if( idx < 0 )
2572
+ continue;
2573
+
2574
+ const nodeLinks = this.editor.graph.links[ linkId ];
2575
+
2576
+ for ( var link of nodeLinks )
2577
+ {
2578
+ if( link.inputIdx != index )
2579
+ continue;
2580
+
2581
+ // This is the value!!
2582
+ return link.data;
2583
+ }
2584
+ }
2585
+ }
2586
+
2587
+ setOutput( index, data ) {
2588
+
2589
+ if( !this.outputs || !this.outputs.length || !this.outputs[ index ] )
2590
+ return;
2591
+
2592
+ // Set data in link
2593
+
2594
+ for( let linkId in this.editor.graph.links )
2595
+ {
2596
+ const idx = linkId.indexOf( this.id + '@' );
2597
+
2598
+ if( idx < 0 )
2599
+ continue;
2600
+
2601
+ const nodeLinks = this.editor.graph.links[ linkId ];
2602
+
2603
+ for ( var link of nodeLinks )
2604
+ {
2605
+ if( link.outputIdx != index )
2606
+ continue;
2607
+
2608
+ if( data != undefined && link.inputType != link.outputType && link.inputType != "any" && link.outputType != "any" )
2609
+ {
2610
+ // In case of supported casting, use function to cast..
2611
+
2612
+ var fn = this.editor.supportedCastTypes[ link.outputType + '@' + link.inputType ];
2613
+
2614
+ // Use function if it's possible to cast!
2615
+
2616
+ data = fn ? fn( LX.deepCopy( data ) ) : null;
2617
+ }
2618
+
2619
+ link.data = data;
2620
+ }
2621
+ }
2622
+ }
2623
+
2624
+ getOutput( index ) {
2625
+
2626
+ return this.outputs[ index ].value;
2627
+ }
2628
+
2629
+ serialize() {
2630
+
2631
+ var o = { };
2632
+
2633
+ o.id = this.id;
2634
+ o.title = this.title;
2635
+ o.color = this.color;
2636
+ o.position = this.position;
2637
+ o.type = this.type;
2638
+ o.inputs = this.inputs;
2639
+ o.outputs = this.outputs;
2640
+ o.properties = this.properties;
2641
+
2642
+ return o;
2643
+ }
2644
+ }
2645
+
2646
+ LX.GraphNode = GraphNode;
2647
+
2648
+ /*
2649
+ ************ PREDEFINED NODES ************
2650
+
2651
+ Nodes can override the following methods:
2652
+
2653
+ - onCreate: Add inputs, outputs and properties
2654
+ - onStart: Callback on graph starts
2655
+ - onStop: Callback on graph stops
2656
+ - onExecute: Callback for node execution
2657
+ */
2658
+
2659
+
2660
+ /*
2661
+ Math nodes
2662
+ */
2663
+
2664
+ class NodeAdd extends GraphNode
2665
+ {
2666
+ onCreate() {
2667
+ this.addInput( null, "float" );
2668
+ this.addInput( null, "float" );
2669
+ this.addOutput( null, "float" );
2670
+ this.addProperty( "A", "float", 0 );
2671
+ this.addProperty( "B", "float", 0 );
2672
+ }
2673
+
2674
+ onExecute() {
2675
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2676
+ var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
2677
+ this.setOutput( 0, a + b );
2678
+ }
2679
+ }
2680
+
2681
+ NodeAdd.description = "Addition of 2 values (A+B)."
2682
+ GraphEditor.registerCustomNode( "math/Add", NodeAdd );
2683
+
2684
+ class NodeSubstract extends GraphNode
2685
+ {
2686
+ onCreate() {
2687
+ this.addInput( null, "float" );
2688
+ this.addInput( null, "float" );
2689
+ this.addOutput( null, "float" );
2690
+ this.addProperty( "A", "float", 0 );
2691
+ this.addProperty( "B", "float", 0 );
2692
+ }
2693
+
2694
+ onExecute() {
2695
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2696
+ var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
2697
+ this.setOutput( 0, a - b );
2698
+ }
2699
+ }
2700
+
2701
+ NodeSubstract.description = "Substraction of 2 values (A-B)."
2702
+ GraphEditor.registerCustomNode( "math/Substract", NodeSubstract );
2703
+
2704
+ class NodeMultiply extends GraphNode
2705
+ {
2706
+ onCreate() {
2707
+ this.addInput( null, "float" );
2708
+ this.addInput( null, "float" );
2709
+ this.addOutput( null, "float" );
2710
+ this.addProperty( "A", "float", 0 );
2711
+ this.addProperty( "B", "float", 0 );
2712
+ }
2713
+
2714
+ onExecute() {
2715
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2716
+ var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
2717
+ this.setOutput( 0, a * b );
2718
+ }
2719
+ }
2720
+
2721
+ NodeMultiply.description = "Multiplication of 2 values (A*B)."
2722
+ GraphEditor.registerCustomNode( "math/Multiply", NodeMultiply );
2723
+
2724
+ class NodeDivide extends GraphNode
2725
+ {
2726
+ onCreate() {
2727
+ this.addInput( null, "float" );
2728
+ this.addInput( null, "float" );
2729
+ this.addOutput( null, "float" );
2730
+ this.addProperty( "A", "float", 0 );
2731
+ this.addProperty( "B", "float", 0 );
2732
+ }
2733
+
2734
+ onExecute() {
2735
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2736
+ var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
2737
+ this.setOutput( 0, a / b );
2738
+ }
2739
+ }
2740
+
2741
+ NodeDivide.description = "Division of 2 values (A/B)."
2742
+ GraphEditor.registerCustomNode( "math/Divide", NodeDivide );
2743
+
2744
+ class NodeSqrt extends GraphNode
2745
+ {
2746
+ onCreate() {
2747
+ this.addInput( null, "float" );
2748
+ this.addOutput( null, "float" );
2749
+ this.addProperty( "Value", "float", 0 );
2750
+ }
2751
+
2752
+ onExecute() {
2753
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2754
+ this.setOutput( 0, Math.sqrt( a ) );
2755
+ }
2756
+ }
2757
+
2758
+ NodeSqrt.description = "Square root of a scalar."
2759
+ GraphEditor.registerCustomNode( "math/SQRT", NodeSqrt );
2760
+
2761
+ /*
2762
+ Math Missing:
2763
+ - abs
2764
+ - ceil
2765
+ - clamp
2766
+ - floor
2767
+ - fract
2768
+ - lerp
2769
+ - log
2770
+ - max
2771
+ - min
2772
+ - negate
2773
+ - pow
2774
+ - remainder
2775
+ - round
2776
+ - remap range
2777
+ - saturate
2778
+ - step
2779
+ */
2780
+
2781
+
2782
+ /*
2783
+ Logical operator nodes
2784
+ */
2785
+
2786
+ class NodeAnd extends GraphNode
2787
+ {
2788
+ onCreate() {
2789
+ this.addInput( null, "bool" );
2790
+ this.addInput( null, "bool" );
2791
+ this.addOutput( null, "bool" );
2792
+ }
2793
+
2794
+ onExecute() {
2795
+ var a = this.getInput( 0 ), b = this.getInput( 1 );
2796
+ if( a == undefined || b == undefined )
2797
+ return;
2798
+ this.setOutput( 0, !!( a ) && !!( b ) );
2799
+ }
2800
+ }
2801
+
2802
+ GraphEditor.registerCustomNode( "logic/And", NodeAnd );
2803
+
2804
+ class NodeOr extends GraphNode
2805
+ {
2806
+ onCreate() {
2807
+ this.addInput( null, "bool" );
2808
+ this.addInput( null, "bool" );
2809
+ this.addOutput( null, "bool" );
2810
+ }
2811
+
2812
+ onExecute() {
2813
+ var a = this.getInput( 0 ), b = this.getInput( 1 );
2814
+ if( a == undefined || b == undefined )
2815
+ return;
2816
+ this.setOutput( 0, !!( a ) || !!( b ) );
2817
+ }
2818
+ }
2819
+
2820
+ GraphEditor.registerCustomNode( "logic/Or", NodeOr );
2821
+
2822
+ class NodeEqual extends GraphNode
2823
+ {
2824
+ onCreate() {
2825
+ this.addInput( null, "float" );
2826
+ this.addInput( null, "float" );
2827
+ this.addOutput( null, "bool" );
2828
+ }
2829
+
2830
+ logicOp( a, b ) {
2831
+ return a == b;
2832
+ }
2833
+
2834
+ onExecute() {
2835
+ var a = this.getInput( 0 ), b = this.getInput( 1 );
2836
+ if( a == undefined || b == undefined )
2837
+ return;
2838
+ this.setOutput( 0, this.logicOp( a, b ) );
2839
+ }
2840
+ }
2841
+
2842
+ GraphEditor.registerCustomNode( "logic/Equal", NodeEqual );
2843
+
2844
+ class NodeNotEqual extends NodeEqual
2845
+ {
2846
+ logicOp( a, b ) {
2847
+ return a != b;
2848
+ }
2849
+ }
2850
+
2851
+ GraphEditor.registerCustomNode( "logic/NotEqual", NodeNotEqual );
2852
+
2853
+ class NodeLess extends NodeEqual
2854
+ {
2855
+ logicOp( a, b ) {
2856
+ return a < b;
2857
+ }
2858
+ }
2859
+
2860
+ GraphEditor.registerCustomNode( "logic/Less", NodeLess );
2861
+
2862
+ class NodeLessEqual extends NodeEqual
2863
+ {
2864
+ logicOp( a, b ) {
2865
+ return a <= b;
2866
+ }
2867
+ }
2868
+
2869
+ GraphEditor.registerCustomNode( "logic/LessEqual", NodeLessEqual );
2870
+
2871
+ class NodeGreater extends NodeEqual
2872
+ {
2873
+ logicOp( a, b ) {
2874
+ return a > b;
2875
+ }
2876
+ }
2877
+
2878
+ GraphEditor.registerCustomNode( "logic/Greater", NodeGreater );
2879
+
2880
+ class NodeGreaterEqual extends NodeEqual
2881
+ {
2882
+ logicOp( a, b ) {
2883
+ return a >= b;
2884
+ }
2885
+ }
2886
+
2887
+ GraphEditor.registerCustomNode( "logic/GreaterEqual", NodeGreaterEqual );
2888
+
2889
+ class NodeSelect extends GraphNode
2890
+ {
2891
+ onCreate() {
2892
+ this.addInput( "True", "any" );
2893
+ this.addInput( "False", "any" );
2894
+ this.addInput( "Condition", "bool" );
2895
+ this.addOutput( null, "any" );
2896
+ }
2897
+
2898
+ onExecute() {
2899
+ var a = this.getInput( 0 ), b = this.getInput( 1 ), cond = this.getInput( 2 );
2900
+ if( a == undefined || b == undefined || cond == undefined )
2901
+ return;
2902
+ this.setOutput( 0, cond ? a : b );
2903
+ }
2904
+ }
2905
+
2906
+ GraphEditor.registerCustomNode( "logic/Select", NodeSelect );
2907
+
2908
+ class NodeCompare extends GraphNode
2909
+ {
2910
+ onCreate() {
2911
+ this.addInput( "A", "any" );
2912
+ this.addInput( "B", "any" );
2913
+ this.addInput( "True", "any" );
2914
+ this.addInput( "False", "any" );
2915
+ this.addProperty( "Condition", "select", 'Equal', [ 'Equal', 'Not Equal', 'Less', 'Less Equal', 'Greater', 'Greater Equal' ] );
2916
+ this.addOutput( null, "any" );
2917
+ }
2918
+
2919
+ onExecute() {
2920
+ var a = this.getInput( 0 ), b = this.getInput( 1 ), TrueVal = this.getInput( 2 ), FalseVal = this.getInput( 3 );;
2921
+ var cond = this.properties[ 0 ].value;
2922
+ if( a == undefined || b == undefined || TrueVal == undefined || FalseVal == undefined )
2923
+ return;
2924
+ var output;
2925
+ switch( cond ) {
2926
+ case 'Equal': output = ( a == b ? TrueVal : FalseVal ); break;
2927
+ case 'Not Equal': output = ( a != b ? TrueVal : FalseVal ); break;
2928
+ case 'Less': output = ( a < b ? TrueVal : FalseVal ); break;
2929
+ case 'Less Equal': output = ( a <= b ? TrueVal : FalseVal ); break;
2930
+ case 'Greater': output = ( a > b ? TrueVal : FalseVal ); break;
2931
+ case 'Greater Equal': output = ( a >= b ? TrueVal : FalseVal ); break;
2932
+ }
2933
+ this.setOutput( 0, output );
2934
+ }
2935
+ }
2936
+ NodeCompare.description = "Compare A to B given the selected operator. If true, return value of True else return value of False."
2937
+ GraphEditor.registerCustomNode( "logic/Compare", NodeCompare );
2938
+
2939
+ /*
2940
+ Event nodes
2941
+ */
2942
+
2943
+ class NodeKeyDown extends GraphNode
2944
+ {
2945
+ onCreate() {
2946
+ this.addOutput( null, "bool" );
2947
+ this.addProperty( "Key", "string", " " );
2948
+ }
2949
+
2950
+ onExecute() {
2951
+ this.setOutput( 0, !!this.editor.keys[ this.properties[ 0 ].value ] );
2952
+ }
2953
+ }
2954
+
2955
+ GraphEditor.registerCustomNode( "events/KeyDown", NodeKeyDown );
2956
+
2957
+ /*
2958
+ Input nodes
2959
+ */
2960
+
2961
+ class NodeString extends GraphNode
2962
+ {
2963
+ onCreate() {
2964
+ this.addOutput( null, "string" );
2965
+ this.addProperty( null, "string", "text" );
2966
+ }
2967
+
2968
+ onExecute() {
2969
+ this.setOutput( 0, this.properties[ 0 ].value );
2970
+ }
2971
+ }
2972
+
2973
+ GraphEditor.registerCustomNode( "inputs/String", NodeString );
2974
+
2975
+ class NodeFloat extends GraphNode
2976
+ {
2977
+ onCreate() {
2978
+ this.addOutput( null, "float" );
2979
+ this.addProperty( null, "float", 0.0 );
2980
+ }
2981
+
2982
+ onExecute() {
2983
+ this.setOutput( 0, this.properties[ 0 ].value );
2984
+ }
2985
+ }
2986
+
2987
+ GraphEditor.registerCustomNode( "inputs/Float", NodeFloat );
2988
+
2989
+ class NodeVector2 extends GraphNode
2990
+ {
2991
+ onCreate() {
2992
+ this.addOutput( "Value", "vec2" );
2993
+ this.addProperty( "Value", "vec2", [ 0, 0 ] );
2994
+ }
2995
+
2996
+ onExecute() {
2997
+ this.setOutput( 0, this.properties[ 0 ].value );
2998
+ }
2999
+ }
3000
+
3001
+ GraphEditor.registerCustomNode( "inputs/Vector2", NodeVector2 );
3002
+
3003
+ class NodeVector3 extends GraphNode
3004
+ {
3005
+ onCreate() {
3006
+ this.addOutput( "Value", "vec3" );
3007
+ this.addProperty( "Value", "vec3", [ 0, 0, 0 ] );
3008
+ }
3009
+
3010
+ onExecute() {
3011
+ this.setOutput( 0, this.properties[ 0 ].value );
3012
+ }
3013
+ }
3014
+
3015
+ GraphEditor.registerCustomNode( "inputs/Vector3", NodeVector3 );
3016
+
3017
+ class NodeVector4 extends GraphNode
3018
+ {
3019
+ onCreate() {
3020
+ this.addOutput( "Value", "vec4" );
3021
+ this.addProperty( "Value", "vec4", [ 0, 0, 0, 0 ] );
3022
+ }
3023
+
3024
+ onExecute() {
3025
+ this.setOutput( 0, this.properties[ 0 ].value );
3026
+ }
3027
+ }
3028
+
3029
+ GraphEditor.registerCustomNode( "inputs/Vector4", NodeVector4 );
3030
+
3031
+ /*
3032
+ Variable nodes
3033
+ */
3034
+
3035
+ class NodeSetVariable extends GraphNode
3036
+ {
3037
+ onCreate() {
3038
+ this.addInput( "Value", "any" );
3039
+ this.addOutput( null, "any" );
3040
+ this.addProperty( "Name", "string", "" );
3041
+ }
3042
+
3043
+ onExecute() {
3044
+ var varName = this.getInput( 0 );
3045
+ if( varName == undefined )
3046
+ return;
3047
+ var varValue = this.getInput( 1 );
3048
+ if( varValue == undefined )
3049
+ return;
3050
+ this.graph.setVariable( varName, varValue );
3051
+ this.setOutput( 0, varValue );
3052
+ }
3053
+ }
3054
+
3055
+ NodeSetVariable.title = "Set Variable";
3056
+ GraphEditor.registerCustomNode( "variables/SetVariable", NodeSetVariable );
3057
+
3058
+ class NodeGetVariable extends GraphNode
3059
+ {
3060
+ onCreate() {
3061
+ this.addOutput( null, "any" );
3062
+ this.addProperty( "Name", "string", "" );
3063
+ }
3064
+
3065
+ onExecute() {
3066
+ var varName = this.getInput( 0 );
3067
+ if( varName == undefined )
3068
+ return;
3069
+ var data = this.editor.getVariable( varName );
3070
+ if( data != undefined )
3071
+ this.setOutput( 0, data );
3072
+ }
3073
+ }
3074
+
3075
+ NodeGetVariable.title = "Get Variable";
3076
+ GraphEditor.registerCustomNode( "variables/GetVariable", NodeGetVariable );
3077
+
3078
+ /*
3079
+ System nodes
3080
+ */
3081
+
3082
+ class NodeConsoleLog extends GraphNode
3083
+ {
3084
+ onCreate() {
3085
+ this.addInput( null, "any" );
3086
+ }
3087
+
3088
+ onExecute() {
3089
+ var data = this.getInput( 0 );
3090
+ if( data == undefined )
3091
+ return;
3092
+ console.log( data );
3093
+ }
3094
+ }
3095
+
3096
+ NodeConsoleLog.title = "Console Log";
3097
+ GraphEditor.registerCustomNode( "system/ConsoleLog", NodeConsoleLog );
3098
+
3099
+ class NodeMain extends GraphNode
3100
+ {
3101
+ onCreate() {
3102
+ this.addInput( "a", "float" );
3103
+ this.addInput( "b", "bool" );
3104
+ this.addInput( "Color", "vec4" );
3105
+ }
3106
+
3107
+ onExecute() {
3108
+ var data = this.getInput( 2 );
3109
+ if( data == undefined )
3110
+ return;
3111
+ console.log( data );
3112
+ };
3113
+ }
3114
+
3115
+ GraphEditor.registerCustomNode( "system/Main", NodeMain );
533
3116
 
534
- export { GraphCanvas, Graph, GraphNode };
3117
+ export { GraphEditor, Graph, GraphNode };