lexgui 0.1.29 → 0.1.31

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.
@@ -4,43 +4,7 @@ if(!LX) {
4
4
  throw("lexgui.js missing!");
5
5
  }
6
6
 
7
- LX.components.push( 'Graph' );
8
-
9
- function flushCss(element) {
10
- // By reading the offsetHeight property, we are forcing
11
- // the browser to flush the pending CSS changes (which it
12
- // does to ensure the value obtained is accurate).
13
- element.offsetHeight;
14
- }
15
-
16
- function swapElements (obj, a, b) {
17
- [obj[a], obj[b]] = [obj[b], obj[a]];
18
- }
19
-
20
- function swapArrayElements (array, id0, id1) {
21
- [array[id0], array[id1]] = [array[id1], array[id0]];
22
- };
23
-
24
- function sliceChar(str, idx) {
25
- return str.substr(0, idx) + str.substr(idx + 1);
26
- }
27
-
28
- function firstNonspaceIndex(str) {
29
- return str.search(/\S|$/);
30
- }
31
-
32
- function deleteElement( el ) {
33
- if( el ) el.remove();
34
- }
35
-
36
- let ASYNC_ENABLED = true;
37
-
38
- function doAsync( fn, ms ) {
39
- if( ASYNC_ENABLED )
40
- setTimeout( fn, ms ?? 0 );
41
- else
42
- fn();
43
- }
7
+ LX.components.push( 'GraphEditor' );
44
8
 
45
9
  class BoundingBox {
46
10
 
@@ -50,7 +14,7 @@ class BoundingBox {
50
14
  this.size = s ?? new LX.vec2( 0, 0 );
51
15
  }
52
16
 
53
- merge ( bb ) {
17
+ merge( bb ) {
54
18
 
55
19
  console.assert( bb.constructor == BoundingBox );
56
20
 
@@ -67,7 +31,7 @@ class BoundingBox {
67
31
  this.size = merge_max.sub( merge_min );
68
32
  }
69
33
 
70
- inside ( bb ) {
34
+ inside( bb, full = true ) {
71
35
 
72
36
  const min_0 = this.origin;
73
37
  const max_0 = this.origin.add( this.size );
@@ -75,8 +39,16 @@ class BoundingBox {
75
39
  const min_1 = bb.origin;
76
40
  const max_1 = bb.origin.add( bb.size );
77
41
 
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;
42
+ if( full )
43
+ {
44
+ return min_1.x >= min_0.x && max_1.x <= max_0.x
45
+ && min_1.y >= min_0.y && max_1.y <= max_0.y;
46
+ }
47
+ else
48
+ {
49
+ return max_1.x >= min_0.x && min_1.x <= max_0.x
50
+ && max_1.y >= min_0.y && min_1.y <= max_0.y;
51
+ }
80
52
  }
81
53
  };
82
54
 
@@ -97,6 +69,10 @@ class GraphEditor {
97
69
  static EVENT_MOUSEWHEEL = 1;
98
70
 
99
71
  static LAST_GROUP_ID = 0;
72
+ static LAST_FUNCTION_ID = 0;
73
+
74
+ static STOPPED = 0;
75
+ static RUNNING = 1;
100
76
 
101
77
  // Node Drawing
102
78
 
@@ -114,10 +90,10 @@ class GraphEditor {
114
90
 
115
91
  GraphEditor.__instances.push( this );
116
92
 
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: () => { } } );
93
+ const useSidebar = options.sidebar ?? true;
94
+
95
+ this._sidebar = area.addSidebar( m => {
96
+ m.add( "Create", { icon: "fa fa-add", bottom: true, callback: (e) => this._onSidebarCreate( e ) } );
121
97
  });
122
98
 
123
99
  this.base_area = area;
@@ -127,12 +103,20 @@ class GraphEditor {
127
103
 
128
104
  this.root = this.area.root;
129
105
  this.root.tabIndex = -1;
106
+
130
107
  area.attach( this.root );
131
108
 
109
+ this._graphContainer = area.sections[ 1 ].root;
110
+ this._sidebarDom = area.sections[ 0 ].root;
111
+ this._sidebarActive = useSidebar;
112
+
113
+ // Set sidebar state depending on options..
114
+ this._toggleSideBar( useSidebar );
115
+
132
116
  // Bind resize
133
117
 
134
118
  area.onresize = ( bb ) => {
135
- console.log(bb);
119
+
136
120
  };
137
121
 
138
122
  area.addOverlayButtons( [
@@ -141,27 +125,20 @@ class GraphEditor {
141
125
  icon: "fa fa-table-columns",
142
126
  callback: () => this._toggleSideBar(),
143
127
  },
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
- // ],
128
+ [
129
+ {
130
+ name: "Start Graph",
131
+ icon: "fa fa-play",
132
+ callback: (value, event) => this.start(),
133
+ selectable: true
134
+ },
135
+ {
136
+ name: "Stop Graph",
137
+ icon: "fa-solid fa-stop",
138
+ callback: (value, event) => this.stop(),
139
+ selectable: true
140
+ }
141
+ ],
165
142
  [
166
143
  {
167
144
  name: "Enable Snapping",
@@ -184,9 +161,14 @@ class GraphEditor {
184
161
  {
185
162
  name: "Export",
186
163
  icon: "fa fa-diagram-project",
187
- callback: (value, event) => this.graph.export()
164
+ callback: (value, event) => this.currentGraph.export()
188
165
  }
189
- ]
166
+ ],
167
+ {
168
+ name: "",
169
+ class: "graph-title",
170
+ callback: (value, event) => this._showRenameGraphDialog()
171
+ }
190
172
  ], { float: "htc" } );
191
173
 
192
174
  this.root.addEventListener( 'keydown', this._processKeyDown.bind( this ), true );
@@ -202,7 +184,7 @@ class GraphEditor {
202
184
  this.root.addEventListener( 'focusout', this._processFocus.bind( this, false ) );
203
185
 
204
186
  this.propertiesDialog = new LX.PocketDialog( "Properties", null, {
205
- size: [ "300px", null ],
187
+ size: [ "350px", null ],
206
188
  position: [ "8px", "8px" ],
207
189
  float: "left",
208
190
  class: 'lexgraphpropdialog'
@@ -236,18 +218,17 @@ class GraphEditor {
236
218
 
237
219
  this.keys = { };
238
220
 
239
- this._sidebar = area.sections[ 0 ].root;
240
- this._sidebarActive = false;
241
-
242
221
  this._snapToGrid = false;
243
222
  this._snapValue = 1.0;
244
223
 
245
- this._scale = 1.0;
224
+ // Graphs, Nodes and connections
246
225
 
247
- // Nodes and connections
226
+ this.currentGraph = null;
248
227
 
249
- this.nodes = { };
250
- this.variables = { };
228
+ this.graphs = { };
229
+ this.nodes = { };
230
+ this.groups = { };
231
+ this.variables = { };
251
232
 
252
233
  this.selectedNodes = [ ];
253
234
 
@@ -273,7 +254,6 @@ class GraphEditor {
273
254
  // Back pattern
274
255
 
275
256
  const f = 15.0;
276
- this._patternPosition = new LX.vec2( 0, 0 );
277
257
  this._patternSize = new LX.vec2( f );
278
258
  this._circlePatternSize = f * 0.04;
279
259
  this._circlePatternColor = '#71717a9c';
@@ -372,6 +352,11 @@ class GraphEditor {
372
352
  node.position = new LX.vec2( 0, 0 );
373
353
  node.color = null;
374
354
 
355
+ if( baseClass.name == 'NodeFunction' )
356
+ {
357
+ node.gid = baseClass.gid;
358
+ }
359
+
375
360
  // Extra options
376
361
  if ( options ) {
377
362
  for (var i in options) {
@@ -393,24 +378,41 @@ class GraphEditor {
393
378
 
394
379
  setGraph( graph ) {
395
380
 
381
+ // Nothing to do, already there...
382
+ if( this.currentGraph && graph.id == this.currentGraph.id )
383
+ return;
384
+
396
385
  this.clear();
397
386
 
398
- this.graph = graph;
387
+ graph.id = graph.id ?? graph.constructor.name + '-' + LX.UTILS.uidGenerator();
388
+
389
+ this.graphs[ graph.id ] = graph;
399
390
 
400
- if( !this.graph.nodes )
391
+ if( !graph.nodes )
401
392
  {
402
393
  console.warn( 'Graph does not contain any node!' );
403
394
  return;
404
395
  }
405
396
 
406
- for( let node of this.graph.nodes )
397
+ this.currentGraph = graph;
398
+
399
+ this._updatePattern();
400
+
401
+ for( let node of graph.nodes )
407
402
  {
408
403
  this._createNodeDOM( node );
409
404
  }
410
405
 
411
- for( let linkId in this.graph.links )
406
+ for( let group of graph.groups )
407
+ {
408
+ const groupDom = this._createGroup( group );
409
+ groupDom.querySelector( '.lexgraphgrouptitle' ).value = group.name;
410
+ this._domNodes.prepend( groupDom );
411
+ }
412
+
413
+ for( let linkId in graph.links )
412
414
  {
413
- const links = this.graph.links[ linkId ];
415
+ const links = graph.links[ linkId ];
414
416
 
415
417
  for( let link of links )
416
418
  {
@@ -418,24 +420,21 @@ class GraphEditor {
418
420
  }
419
421
  }
420
422
 
421
- // TODO: REMOVE THIS (DEBUG)
422
- this.start();
423
- //
423
+ this._updateGraphName( graph.name );
424
+ this._togglePropertiesDialog( false );
424
425
  }
425
426
 
426
427
  /**
427
428
  * @method loadGraph
428
- * @param {Graph} graph
429
+ * @param {String} url
430
+ * @param {Function} callback Function to call once the graph is loaded
429
431
  */
430
432
 
431
433
  loadGraph( url, callback ) {
432
434
 
433
435
  const onComplete = ( json ) => {
434
436
 
435
- var graph = new Graph();
436
- graph.configure( json );
437
-
438
- this.setGraph( graph );
437
+ let graph = ( json.type == 'Graph' ) ? this.addGraph( json ) : this.addGraphFunction( json );
439
438
 
440
439
  if( callback )
441
440
  callback( graph );
@@ -446,6 +445,86 @@ class GraphEditor {
446
445
  LX.requestJSON( url, onComplete, onError );
447
446
  }
448
447
 
448
+ /**
449
+ * @method addGraph
450
+ * @param {Object} o Options to configure the graph
451
+ */
452
+
453
+ addGraph( o ) {
454
+
455
+ let graph = new Graph();
456
+ graph.editor = this;
457
+
458
+ if( o )
459
+ {
460
+ // Load functions first if any..
461
+
462
+ for( let fn of o.functions ?? [] )
463
+ {
464
+ this.addGraphFunction( fn );
465
+ }
466
+
467
+ graph.configure( o );
468
+ }
469
+
470
+ this.setGraph( graph );
471
+
472
+ this._sidebar.add( graph.name, { icon: "fa fa-diagram-project", className: graph.id, callback: (e) => { this.setGraph( graph ) } } );
473
+
474
+ this._sidebar.select( graph.name );
475
+
476
+ return graph;
477
+ }
478
+
479
+ /**
480
+ * @method addGraphFunction
481
+ * @param {Object} o Options to configure the graph
482
+ */
483
+
484
+ addGraphFunction( o ) {
485
+
486
+ let func = new GraphFunction();
487
+ func.editor = this;
488
+
489
+ if( o )
490
+ {
491
+ // Load other inner functions first if any..
492
+
493
+ for( let fn of o.functions ?? [] )
494
+ {
495
+ this.addGraphFunction( fn );
496
+ }
497
+
498
+ func.configure( o );
499
+ }
500
+
501
+ this.setGraph( func );
502
+
503
+ // Add a new node to use this function..
504
+
505
+ class NodeFunction extends GraphNode
506
+ {
507
+ onCreate() {
508
+ this.addInput( null, "float" );
509
+ this.addOutput( null, "any" );
510
+ }
511
+
512
+ onExecute() {
513
+ const func = NodeFunction.func;
514
+ const value = func.getOutputData( this.getInput( 0 ) );
515
+ this.setOutput( 0, value );
516
+ }
517
+ }
518
+
519
+ NodeFunction.func = func;
520
+ NodeFunction.gid = func.id;
521
+ GraphEditor.registerCustomNode( "function/" + func.name, NodeFunction );
522
+
523
+ this._sidebar.add( func.name, { icon: "fa fa-florin-sign", className: func.id, callback: (e) => { this.setGraph( func ) } } );
524
+
525
+ this._sidebar.select( func.name );
526
+ }
527
+
449
528
  /**
450
529
  * @method clear
451
530
  */
@@ -470,10 +549,10 @@ class GraphEditor {
470
549
 
471
550
  propagateEventToAllNodes( eventName, params ) {
472
551
 
473
- if( !this.graph )
552
+ if( !this.currentGraph )
474
553
  return;
475
554
 
476
- for ( let node of this.graph.nodes )
555
+ for ( let node of this.currentGraph.nodes )
477
556
  {
478
557
  if( !node[ eventName ] )
479
558
  continue;
@@ -511,6 +590,7 @@ class GraphEditor {
511
590
  _createNodeDOM( node ) {
512
591
 
513
592
  node.editor = this;
593
+ node.graphID = this.currentGraph.id;
514
594
 
515
595
  var nodeContainer = document.createElement( 'div' );
516
596
  nodeContainer.classList.add( 'lexgraphnode' );
@@ -527,6 +607,12 @@ class GraphEditor {
527
607
  const category = node.constructor.category;
528
608
  nodeContainer.classList.add( category );
529
609
  }
610
+ else
611
+ {
612
+ const pos = node.type.lastIndexOf( "/" );
613
+ const category = node.type.substring( 0, pos );
614
+ nodeContainer.classList.add( category );
615
+ }
530
616
 
531
617
  // Update with manual color
532
618
 
@@ -582,14 +668,16 @@ class GraphEditor {
582
668
  LX.addContextMenu(null, e, m => {
583
669
 
584
670
  m.add( "Copy", () => {
585
- // TODO
586
- // ...
671
+ this._clipboardData = {
672
+ id: node.id,
673
+ gid: this.currentGraph.id
674
+ };
587
675
  } );
588
676
 
589
- m.add( "Paste", () => {
590
- // TODO
591
- // ...
592
- } );
677
+ // TODO
678
+ // m.add( "Paste", () => {
679
+
680
+ // } );
593
681
 
594
682
  m.add( "" );
595
683
 
@@ -604,6 +692,13 @@ class GraphEditor {
604
692
  // Only for left click..
605
693
  if( e.button != LX.MOUSE_LEFT_CLICK )
606
694
  return;
695
+
696
+ // Open graph function..
697
+ if( node.constructor.func )
698
+ {
699
+ this._sidebar.select( node.constructor.func.name )
700
+ }
701
+
607
702
  } );
608
703
 
609
704
  // Title header
@@ -613,7 +708,7 @@ class GraphEditor {
613
708
  nodeContainer.appendChild( nodeHeader );
614
709
 
615
710
  // Properties
616
- // if( node.properties.length && node.constructor.category == 'inputs' )
711
+ // if( node.properties.length )
617
712
  // {
618
713
  // var nodeProperties = document.createElement( 'div' );
619
714
  // nodeProperties.classList.add( 'lexgraphnodeproperties' );
@@ -656,7 +751,7 @@ class GraphEditor {
656
751
  {
657
752
  var nodeInputs = null;
658
753
 
659
- if( node.inputs && node.inputs.length )
754
+ if( hasInputs )
660
755
  {
661
756
  nodeInputs = document.createElement( 'div' );
662
757
  nodeInputs.classList.add( 'lexgraphnodeinputs' );
@@ -678,6 +773,7 @@ class GraphEditor {
678
773
 
679
774
  var type = document.createElement( 'span' );
680
775
  type.className = 'io__type input ' + i.type;
776
+ type.dataset[ 'type' ] = i.type;
681
777
  type.innerHTML = '<span>' + i.type[ 0 ].toUpperCase() + '</span>';
682
778
  input.appendChild( type );
683
779
 
@@ -702,7 +798,7 @@ class GraphEditor {
702
798
  {
703
799
  var nodeOutputs = null;
704
800
 
705
- if( node.outputs && node.outputs.length )
801
+ if( hasOutputs )
706
802
  {
707
803
  nodeOutputs = document.createElement( 'div' );
708
804
  nodeOutputs.classList.add( 'lexgraphnodeoutputs' );
@@ -731,6 +827,7 @@ class GraphEditor {
731
827
 
732
828
  var type = document.createElement( 'span' );
733
829
  type.className = 'io__type output ' + o.type;
830
+ type.dataset[ 'type' ] = o.type;
734
831
  type.innerHTML = '<span>' + o.type[ 0 ].toUpperCase() + '</span>';
735
832
  output.appendChild( type );
736
833
 
@@ -750,6 +847,141 @@ class GraphEditor {
750
847
  onDragStart: this._onDragNode.bind( this )
751
848
  } );
752
849
 
850
+ this._addNodeIOEvents( nodeContainer );
851
+
852
+ const id = node.id ?? node.title.toLowerCase().replaceAll( /\s/g, '-' ) + '-' + LX.UTILS.uidGenerator();
853
+ this.nodes[ id ] = { data: node, dom: nodeContainer };
854
+
855
+ node.id = id;
856
+ nodeContainer.dataset[ 'id' ] = id;
857
+
858
+ this._domNodes.appendChild( nodeContainer );
859
+
860
+ // Only 1 main per graph!
861
+ if( node.title == 'Main' )
862
+ {
863
+ this.main = id;
864
+ }
865
+
866
+ node.size = new LX.vec2( nodeContainer.offsetWidth, nodeContainer.offsetHeight );
867
+
868
+ node.resizeObserver = new ResizeObserver( entries => {
869
+
870
+ for( const entry of entries ) {
871
+ const bb = entry.contentRect;
872
+ if( !bb.width || !bb.height )
873
+ continue;
874
+ node.size = new LX.vec2( nodeContainer.offsetWidth, nodeContainer.offsetHeight );
875
+ }
876
+ });
877
+
878
+ node.resizeObserver.observe( nodeContainer );
879
+
880
+ return nodeContainer;
881
+ }
882
+
883
+ _updateNodeDOMIOs( dom, node ) {
884
+
885
+ // Inputs and outputs
886
+ var nodeIO = dom.querySelector( '.lexgraphnodeios' );
887
+
888
+ const hasInputs = node.inputs && node.inputs.length;
889
+ const hasOutputs = node.outputs && node.outputs.length;
890
+
891
+ // Inputs
892
+ {
893
+ var nodeInputs = null;
894
+
895
+ if( hasInputs )
896
+ {
897
+ nodeInputs = nodeIO.querySelector( '.lexgraphnodeinputs' );
898
+ nodeInputs.innerHTML = "";
899
+ }
900
+
901
+ for( let i of node.inputs )
902
+ {
903
+ if( !i.type )
904
+ {
905
+ console.warn( `Missing type for node [${ node.title }], skipping...` );
906
+ continue;
907
+ }
908
+
909
+ var input = document.createElement( 'div' );
910
+ input.className = 'lexgraphnodeio ioinput';
911
+ input.dataset[ 'index' ] = nodeInputs.childElementCount;
912
+
913
+ var type = document.createElement( 'span' );
914
+ type.className = 'io__type input ' + i.type;
915
+ type.innerHTML = '<span>' + i.type[ 0 ].toUpperCase() + '</span>';
916
+ input.appendChild( type );
917
+
918
+ var typeDesc = document.createElement( 'span' );
919
+ typeDesc.className = 'io__typedesc input ' + i.type;
920
+ typeDesc.innerHTML = i.type;
921
+ input.appendChild( typeDesc );
922
+
923
+ if( i.name )
924
+ {
925
+ var name = document.createElement( 'span' );
926
+ name.classList.add( 'io__name' );
927
+ name.innerText = i.name;
928
+ input.appendChild( name );
929
+ }
930
+
931
+ nodeInputs.appendChild( input );
932
+ }
933
+ }
934
+
935
+ // Outputs
936
+ {
937
+ var nodeOutputs = null;
938
+
939
+ if( hasOutputs )
940
+ {
941
+ nodeOutputs = nodeIO.querySelector( '.lexgraphnodeoutputs' );
942
+ nodeOutputs.innerHTML = "";
943
+ }
944
+
945
+ for( let o of node.outputs )
946
+ {
947
+ if( !o.type )
948
+ {
949
+ console.warn( `Missing type for node [${ node.title }], skipping...` );
950
+ }
951
+
952
+ var output = document.createElement( 'div' );
953
+ output.className = 'lexgraphnodeio iooutput';
954
+ output.dataset[ 'index' ] = nodeOutputs.childElementCount;
955
+
956
+ if( o.name )
957
+ {
958
+ var name = document.createElement( 'span' );
959
+ name.classList.add( 'io__name' );
960
+ name.innerText = o.name;
961
+ output.appendChild( name );
962
+ }
963
+
964
+ var type = document.createElement( 'span' );
965
+ type.className = 'io__type output ' + o.type;
966
+ type.innerHTML = '<span>' + o.type[ 0 ].toUpperCase() + '</span>';
967
+ output.appendChild( type );
968
+
969
+ var typeDesc = document.createElement( 'span' );
970
+ typeDesc.className = 'io__typedesc output ' + o.type;
971
+ typeDesc.innerHTML = o.type;
972
+ output.appendChild( typeDesc );
973
+
974
+ nodeOutputs.appendChild( output );
975
+ }
976
+ }
977
+
978
+ this._addNodeIOEvents( dom );
979
+ }
980
+
981
+ _addNodeIOEvents( nodeContainer ) {
982
+
983
+ const nodeIO = nodeContainer.querySelector( '.lexgraphnodeios' );
984
+
753
985
  // Manage links
754
986
 
755
987
  nodeIO.querySelectorAll( '.lexgraphnodeio' ).forEach( el => {
@@ -775,8 +1007,11 @@ class GraphEditor {
775
1007
 
776
1008
  el.addEventListener( 'mouseup', e => {
777
1009
 
1010
+ e.stopPropagation();
1011
+ e.stopImmediatePropagation();
1012
+
778
1013
  // Single click..
779
- if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1014
+ if( ( LX.getTime() - this.lastMouseDown ) < 200 ) {
780
1015
  delete this._generatingLink;
781
1016
  return;
782
1017
  }
@@ -787,14 +1022,11 @@ class GraphEditor {
787
1022
  if( !this._onLink( e ) )
788
1023
  {
789
1024
  // Delete entire SVG if not a successful connection..
790
- deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
1025
+ LX.UTILS.deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
791
1026
  }
792
1027
 
793
1028
  delete this._generatingLink;
794
1029
  }
795
-
796
- e.stopPropagation();
797
- e.stopImmediatePropagation();
798
1030
  } );
799
1031
 
800
1032
  el.addEventListener( 'click', e => {
@@ -808,36 +1040,29 @@ class GraphEditor {
808
1040
  } );
809
1041
 
810
1042
  } );
1043
+ }
811
1044
 
812
- const id = node.id ?? node.title.toLowerCase().replaceAll( /\s/g, '-' ) + '-' + LX.UTILS.uidGenerator();
813
- this.nodes[ id ] = { data: node, dom: nodeContainer };
1045
+ _getAllDOMNodes( includeGroups, exclude ) {
814
1046
 
815
- node.id = id;
816
- nodeContainer.dataset[ 'id' ] = id;
1047
+ var elements = null;
817
1048
 
818
- this._domNodes.appendChild( nodeContainer );
1049
+ if( includeGroups )
1050
+ elements = Array.from( this._domNodes.childNodes );
1051
+ else
1052
+ elements = Array.from( this._domNodes.childNodes ).filter( v => v.classList.contains( 'lexgraphnode' ) );
819
1053
 
820
- // Only 1 main per graph!
821
- if( node.title == 'Main' )
1054
+ if( exclude )
822
1055
  {
823
- this.main = nodeContainer;
1056
+ elements = elements.filter( v => v != exclude );
824
1057
  }
825
1058
 
826
- return nodeContainer;
827
- }
828
-
829
- _getAllDOMNodes( includeGroups ) {
830
-
831
- if( includeGroups )
832
- return this._domNodes.childNodes;
833
-
834
- return Array.from( this._domNodes.childNodes ).filter( v => v.classList.contains( 'lexgraphnode' ) );
1059
+ return elements;
835
1060
  }
836
1061
 
837
1062
  _onMoveNodes( target ) {
838
1063
 
839
1064
  let dT = this._snapToGrid ? this._snappedDeltaMousePosition : this._deltaMousePosition;
840
- dT.div( this._scale, dT);
1065
+ dT.div( this.currentGraph.scale, dT);
841
1066
 
842
1067
  for( let nodeId of this.selectedNodes )
843
1068
  {
@@ -867,17 +1092,20 @@ class GraphEditor {
867
1092
  return;
868
1093
 
869
1094
  let dT = this._snapToGrid ? this._snappedDeltaMousePosition : this._deltaMousePosition;
870
- dT.div( this._scale, dT);
1095
+ dT.div( this.currentGraph.scale, dT);
871
1096
 
872
1097
  this._translateNode( target, dT );
873
1098
 
874
1099
  for( let nodeId of groupNodeIds )
875
1100
  {
876
- const el = this._getNodeDOMElement( nodeId );
1101
+ const isGroup = nodeId.constructor !== String;
877
1102
 
878
- this._translateNode( el, dT );
1103
+ const el = isGroup ? nodeId : this._getNodeDOMElement( nodeId );
879
1104
 
880
- this._updateNodeLinks( nodeId );
1105
+ this._translateNode( el, dT, !isGroup );
1106
+
1107
+ if( !isGroup )
1108
+ this._updateNodeLinks( nodeId );
881
1109
  }
882
1110
  }
883
1111
 
@@ -889,7 +1117,7 @@ class GraphEditor {
889
1117
 
890
1118
  const groupNodeIds = [ ];
891
1119
 
892
- for( let dom of this._getAllDOMNodes() )
1120
+ for( let dom of this._getAllDOMNodes( true, target ) )
893
1121
  {
894
1122
  const x = parseFloat( dom.style.left );
895
1123
  const y = parseFloat( dom.style.top );
@@ -898,7 +1126,7 @@ class GraphEditor {
898
1126
  if( !group_bb.inside( node_bb ) )
899
1127
  continue;
900
1128
 
901
- groupNodeIds.push( dom.dataset[ 'id' ] );
1129
+ groupNodeIds.push( dom.dataset[ 'id' ] ?? dom );
902
1130
  }
903
1131
 
904
1132
  target.nodes = groupNodeIds;
@@ -966,6 +1194,16 @@ class GraphEditor {
966
1194
  case 'select':
967
1195
  panel.addDropdown( p.name, p.options, p.value, (v) => { p.value = v } );
968
1196
  break;
1197
+ case 'array':
1198
+ panel.addArray( p.name, p.value, (v) => {
1199
+ p.value = v;
1200
+ if( node.type == "function/Input" )
1201
+ {
1202
+ node.setOutputs( v );
1203
+ this._updateNodeDOMIOs( dom, node );
1204
+ }
1205
+ }, { innerValues: p.options } );
1206
+ break;
969
1207
  }
970
1208
  }
971
1209
 
@@ -984,7 +1222,7 @@ class GraphEditor {
984
1222
  this._togglePropertiesDialog( false );
985
1223
  }
986
1224
 
987
- _translateNode( dom, deltaTranslation ) {
1225
+ _translateNode( dom, deltaTranslation, updateBasePosition = true ) {
988
1226
 
989
1227
  const translation = deltaTranslation.add( new LX.vec2( parseFloat( dom.style.left ), parseFloat( dom.style.top ) ) );
990
1228
 
@@ -998,27 +1236,44 @@ class GraphEditor {
998
1236
 
999
1237
  dom.style.left = ( translation.x ) + "px";
1000
1238
  dom.style.top = ( translation.y ) + "px";
1239
+
1240
+ // Update base node position..
1241
+ if( updateBasePosition && dom.dataset[ 'id' ] )
1242
+ {
1243
+ let baseNode = this.nodes[ dom.dataset[ 'id' ] ];
1244
+ baseNode.data.position = translation;
1245
+ }
1001
1246
  }
1002
1247
 
1003
1248
  _deleteNode( nodeId ) {
1004
1249
 
1005
- const el = this._getNodeDOMElement( nodeId );
1250
+ const nodeInfo = this.nodes[ nodeId ];
1251
+ const node = nodeInfo.data;
1252
+ const el = nodeInfo.dom;
1006
1253
 
1007
1254
  console.assert( el );
1008
1255
 
1009
- if( el == this.main )
1256
+ if( node.constructor.blockDelete )
1010
1257
  {
1011
- console.warn( `Can't delete MAIN node!` );
1258
+ console.warn( `Can't delete node!` );
1012
1259
  return;
1013
1260
  }
1014
1261
 
1015
- deleteElement( el );
1262
+ LX.UTILS.deleteElement( el );
1263
+
1264
+ // Delete from the editor
1016
1265
 
1017
1266
  delete this.nodes[ nodeId ];
1018
1267
 
1268
+ // Delete from the graph data
1269
+
1270
+ const idx = this.currentGraph.nodes.findIndex( v => v.id === nodeId );
1271
+ console.assert( idx >= 0 );
1272
+ this.currentGraph.nodes.splice( idx, 1 );
1273
+
1019
1274
  // Delete connected links..
1020
1275
 
1021
- for( let key in this.graph.links )
1276
+ for( let key in this.currentGraph.links )
1022
1277
  {
1023
1278
  if( !key.includes( nodeId ) )
1024
1279
  continue;
@@ -1028,13 +1283,13 @@ class GraphEditor {
1028
1283
 
1029
1284
  // Remove the connection from the other before deleting..
1030
1285
 
1031
- const numLinks = this.graph.links[ key ].length;
1286
+ const numLinks = this.currentGraph.links[ key ].length;
1032
1287
 
1033
1288
  for( var i = 0; i < numLinks; ++i )
1034
1289
  {
1035
- var link = this.graph.links[ key ][ i ];
1290
+ var link = this.currentGraph.links[ key ][ i ];
1036
1291
 
1037
- deleteElement( link.path.parentElement );
1292
+ LX.UTILS.deleteElement( link.path.parentElement );
1038
1293
 
1039
1294
  const targetNodeId = targetIsInput ? link.inputNode : link.outputNode;
1040
1295
 
@@ -1057,19 +1312,60 @@ class GraphEditor {
1057
1312
  {
1058
1313
  var active = false;
1059
1314
  for( var links of io.links )
1060
- for( var j of links ){
1061
- console.log(j)
1315
+ {
1316
+ if( !links )
1317
+ continue;
1318
+ for( var j of links ) {
1062
1319
  active |= ( !!j );
1063
1320
  }
1321
+ }
1064
1322
  if( !active )
1065
1323
  delete io.dataset[ 'active' ];
1066
1324
  }
1067
1325
  }
1068
1326
 
1069
- delete this.graph.links[ key ];
1327
+ delete this.currentGraph.links[ key ];
1070
1328
  }
1071
1329
  }
1072
1330
 
1331
+ _deleteGroup( groupId ) {
1332
+
1333
+ const dom = this.groups[ groupId ];
1334
+ LX.UTILS.deleteElement( dom );
1335
+
1336
+ // Delete from the editor
1337
+
1338
+ delete this.groups[ groupId ];
1339
+
1340
+ // Delete from the graph data
1341
+
1342
+ const idx = this.currentGraph.groups.findIndex( v => v.id === groupId );
1343
+ console.assert( idx >= 0 );
1344
+ this.currentGraph.groups.splice( idx, 1 );
1345
+ }
1346
+
1347
+ _cloneNode( nodeId, graphId, position ) {
1348
+
1349
+ const graph = this.graphs[ graphId ?? this.currentGraph.id ];
1350
+
1351
+ const nodeData = graph.getNodeById( nodeId );
1352
+
1353
+ if( !nodeData )
1354
+ return;
1355
+
1356
+ const el = this._getNodeDOMElement( nodeId );
1357
+ const newNode = GraphEditor.addNode( nodeData.type );
1358
+ newNode.properties = LX.deepCopy( nodeData.properties );
1359
+
1360
+ const newDom = this._createNodeDOM( newNode );
1361
+
1362
+ this._translateNode( newDom, position ?? this._getNodePosition( el ) );
1363
+
1364
+ this._selectNode( newDom, true );
1365
+
1366
+ this.currentGraph.nodes.push( newNode );
1367
+ }
1368
+
1073
1369
  _cloneNodes() {
1074
1370
 
1075
1371
  // Clone all selected nodes
@@ -1079,20 +1375,7 @@ class GraphEditor {
1079
1375
 
1080
1376
  for( let nodeId of selectedIds )
1081
1377
  {
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 );
1378
+ this._cloneNode( nodeId );
1096
1379
  }
1097
1380
  }
1098
1381
 
@@ -1110,7 +1393,7 @@ class GraphEditor {
1110
1393
  _getLinks( nodeSrcId, nodeDstId ) {
1111
1394
 
1112
1395
  const str = nodeSrcId + '@' + nodeDstId;
1113
- return this.graph.links[ str ];
1396
+ return this.currentGraph.links[ str ];
1114
1397
  }
1115
1398
 
1116
1399
  _deleteLinks( nodeId, io ) {
@@ -1132,7 +1415,7 @@ class GraphEditor {
1132
1415
  var links = this._getLinks( targetId, nodeId );
1133
1416
 
1134
1417
  var linkIdx = links.findIndex( i => ( i.inputIdx == srcIndex && i.outputIdx == targetIndex ) );
1135
- deleteElement( links[ linkIdx ].path.parentElement );
1418
+ LX.UTILS.deleteElement( links[ linkIdx ].path.parentElement );
1136
1419
  links.splice( linkIdx, 1 );
1137
1420
 
1138
1421
  // Input has no longer any connected link
@@ -1182,7 +1465,7 @@ class GraphEditor {
1182
1465
  var links = this._getLinks( nodeId, targetId );
1183
1466
 
1184
1467
  var linkIdx = links.findIndex( i => ( i.inputIdx == targetIndex && i.outputIdx == srcIndex ) );
1185
- deleteElement( links[ linkIdx ].path.parentElement );
1468
+ LX.UTILS.deleteElement( links[ linkIdx ].path.parentElement );
1186
1469
  links.splice( linkIdx, 1 );
1187
1470
 
1188
1471
  // Remove a connection from the output connections
@@ -1274,7 +1557,7 @@ class GraphEditor {
1274
1557
 
1275
1558
  if( this._snapToGrid )
1276
1559
  {
1277
- const snapSize = this._patternSize.x * this._snapValue * this._scale;
1560
+ const snapSize = this._patternSize.x * this._snapValue * this.currentGraph.scale;
1278
1561
  snapPosition.x = Math.floor( snapPosition.x / snapSize ) * snapSize;
1279
1562
  snapPosition.y = Math.floor( snapPosition.y / snapSize ) * snapSize;
1280
1563
  this._snappedDeltaMousePosition = snapPosition.sub( this._lastSnappedMousePosition );
@@ -1291,7 +1574,8 @@ class GraphEditor {
1291
1574
 
1292
1575
  else if( e.type == 'mouseup' )
1293
1576
  {
1294
- if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1577
+ if( ( LX.getTime() - this.lastMouseDown ) < 200 ) {
1578
+
1295
1579
  this._processClick( e );
1296
1580
  }
1297
1581
 
@@ -1323,7 +1607,7 @@ class GraphEditor {
1323
1607
 
1324
1608
  e.preventDefault();
1325
1609
 
1326
- if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1610
+ if( ( LX.getTime() - this.lastMouseDown ) < 300 ) {
1327
1611
  this._processContextMenu( e );
1328
1612
  }
1329
1613
  }
@@ -1374,8 +1658,14 @@ class GraphEditor {
1374
1658
  // It the event reaches this, the link isn't valid..
1375
1659
  if( this._generatingLink )
1376
1660
  {
1377
- deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
1661
+ const linkInfo = Object.assign( { }, this._generatingLink );
1662
+
1663
+ // Delete old link
1664
+ LX.UTILS.deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
1378
1665
  delete this._generatingLink;
1666
+
1667
+ // Open contextmenu to auto-connect something..
1668
+ this._processContextMenu( e, linkInfo );
1379
1669
  }
1380
1670
 
1381
1671
  else if( this._boxSelecting )
@@ -1385,7 +1675,7 @@ class GraphEditor {
1385
1675
 
1386
1676
  this._selectNodesInBox( this._boxSelecting, this._mousePosition, e.altKey );
1387
1677
 
1388
- deleteElement( this._currentBoxSelectionSVG );
1678
+ LX.UTILS.deleteElement( this._currentBoxSelectionSVG );
1389
1679
 
1390
1680
  delete this._currentBoxSelectionSVG;
1391
1681
  delete this._boxSelecting;
@@ -1399,7 +1689,7 @@ class GraphEditor {
1399
1689
 
1400
1690
  if( rightPressed )
1401
1691
  {
1402
- this._patternPosition.add( this._deltaMousePosition.div( this._scale ), this._patternPosition );
1692
+ this.currentGraph.translation.add( this._deltaMousePosition.div( this.currentGraph.scale ), this.currentGraph.translation );
1403
1693
 
1404
1694
  this._updatePattern();
1405
1695
 
@@ -1435,10 +1725,10 @@ class GraphEditor {
1435
1725
 
1436
1726
  const delta = e.deltaY;
1437
1727
 
1438
- if( delta > 0.0 ) this._scale *= 0.9;
1439
- else this._scale *= ( 1.0 / 0.9 );
1728
+ if( delta > 0.0 ) this.currentGraph.scale *= 0.9;
1729
+ else this.currentGraph.scale *= ( 1.0 / 0.9 );
1440
1730
 
1441
- this._scale = LX.UTILS.clamp( this._scale, GraphEditor.MIN_SCALE, GraphEditor.MAX_SCALE );
1731
+ this.currentGraph.scale = LX.UTILS.clamp( this.currentGraph.scale, GraphEditor.MIN_SCALE, GraphEditor.MAX_SCALE );
1442
1732
 
1443
1733
  // Compute zoom center in pattern space using new scale
1444
1734
  // and get delta..
@@ -1447,17 +1737,46 @@ class GraphEditor {
1447
1737
 
1448
1738
  const deltaCenter = newCenter.sub( center );
1449
1739
 
1450
- this._patternPosition = this._patternPosition.add( deltaCenter );
1740
+ this.currentGraph.translation.add( deltaCenter, this.currentGraph.translation );
1451
1741
 
1452
- this._updatePattern( GraphEditor.EVENT_MOUSEWHEEL );
1742
+ this._updatePattern();
1453
1743
  }
1454
1744
 
1455
- _processContextMenu( e ) {
1745
+ _processContextMenu( e, autoConnect ) {
1456
1746
 
1457
- LX.addContextMenu( "ADD NODE", e, m => {
1458
-
1747
+ LX.addContextMenu( null, e, m => {
1748
+
1749
+ var eventPosition = null;
1750
+
1751
+ if( e )
1752
+ {
1753
+ const rect = this.root.getBoundingClientRect();
1754
+
1755
+ const localPosition = new LX.vec2( e.clientX - rect.x, e.clientY - rect.y );
1756
+
1757
+ eventPosition = this._getPatternPosition( localPosition );
1758
+ }
1759
+
1760
+ if( this._clipboardData )
1761
+ {
1762
+ m.add( "Paste", () => {
1763
+
1764
+ const nodeId = this._clipboardData.id;
1765
+ const graphId = this._clipboardData.gid;
1766
+
1767
+ this._cloneNode( nodeId, graphId, eventPosition );
1768
+
1769
+ } );
1770
+ m.add( "" );
1771
+ }
1772
+
1459
1773
  for( let type in GraphEditor.NODE_TYPES )
1460
1774
  {
1775
+ const baseClass = GraphEditor.NODE_TYPES[ type ];
1776
+
1777
+ if( baseClass.blockAdd )
1778
+ continue;
1779
+
1461
1780
  m.add( type, () => {
1462
1781
 
1463
1782
  const newNode = GraphEditor.addNode( type );
@@ -1469,18 +1788,38 @@ class GraphEditor {
1469
1788
  dom.mustSnap = true;
1470
1789
  }
1471
1790
 
1472
- if( e )
1791
+ if( eventPosition )
1473
1792
  {
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 );
1793
+ this._translateNode( dom, eventPosition );
1481
1794
  }
1482
1795
 
1483
- this.graph.nodes.push( newNode );
1796
+ this.currentGraph.nodes.push( newNode );
1797
+
1798
+ if( autoConnect && newNode.inputs.length )
1799
+ {
1800
+ const srcId = autoConnect.domEl.dataset[ 'id' ];
1801
+ const srcType = autoConnect.io.childNodes[ autoConnect.index ].dataset[ 'type' ];
1802
+ const srcIsInput = autoConnect.ioType == GraphEditor.NODE_IO_INPUT;
1803
+
1804
+ const newLink = {
1805
+ inputNode: srcIsInput ? srcId : newNode.id,
1806
+ inputIdx: srcIsInput ? autoConnect.index : 0,
1807
+ inputType: srcIsInput ? srcType : newNode.inputs[ 0 ].type,
1808
+ outputNode: srcIsInput ? newNode.id : srcId,
1809
+ outputIdx: srcIsInput ? 0 : autoConnect.index,
1810
+ outputType: srcIsInput ? newNode.inputs[ 0 ].type : srcType,
1811
+ }
1812
+
1813
+ // Store link
1814
+
1815
+ const pathId = newLink.outputNode + '@' + newLink.inputNode;
1816
+
1817
+ if( !this.currentGraph.links[ pathId ] ) this.currentGraph.links[ pathId ] = [];
1818
+
1819
+ this.currentGraph.links[ pathId ].push( newLink );
1820
+
1821
+ this._createLink( newLink );
1822
+ }
1484
1823
 
1485
1824
  } );
1486
1825
  }
@@ -1494,101 +1833,40 @@ class GraphEditor {
1494
1833
  start() {
1495
1834
 
1496
1835
  this.mustStop = false;
1836
+ this.state = GraphEditor.RUNNING;
1497
1837
 
1498
1838
  this.propagateEventToAllNodes( 'onStart' );
1499
1839
 
1500
1840
  requestAnimationFrame( this._frame.bind(this) );
1501
1841
  }
1502
1842
 
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 );
1843
+ /**
1844
+ * @method stop
1845
+ */
1566
1846
 
1567
- this._executionNodes.push( preNodeId );
1847
+ stop() {
1568
1848
 
1569
- addNode( preNodeId );
1570
- }
1571
- };
1849
+ this.mustStop = true;
1850
+ this.state = GraphEditor.STOPPED;
1572
1851
 
1573
- // TODO: Search "no output" nodes and add to the executable list (same as main)..
1574
- // ...
1852
+ this.propagateEventToAllNodes( 'onStop' );
1853
+ }
1575
1854
 
1576
- this._executionNodes.push( mainId );
1855
+ /**
1856
+ * @method _frame
1857
+ */
1577
1858
 
1578
- addNode( mainId );
1859
+ _frame() {
1579
1860
 
1580
- for( var i = this._executionNodes.length - 1; i >= 0; --i )
1861
+ if( this.mustStop )
1581
1862
  {
1582
- const node = this.nodes[ this._executionNodes[ i ] ];
1583
-
1584
- if( node.data.onBeforeStep )
1585
- node.data.onBeforeStep();
1863
+ return;
1864
+ }
1586
1865
 
1587
- node.data.execute();
1866
+ requestAnimationFrame( this._frame.bind(this) );
1588
1867
 
1589
- if( node.data.onBeforeStep )
1590
- node.data.onAfterStep();
1591
- }
1868
+ // Only run here main graph!
1869
+ this.currentGraph._runStep( this.main );
1592
1870
  }
1593
1871
 
1594
1872
  _generatePattern() {
@@ -1597,8 +1875,8 @@ class GraphEditor {
1597
1875
  {
1598
1876
  var pattern = document.createElementNS( 'http://www.w3.org/2000/svg', 'pattern' );
1599
1877
  pattern.setAttribute( 'id', 'pattern-0' );
1600
- pattern.setAttribute( 'x', this._patternPosition.x );
1601
- pattern.setAttribute( 'y', this._patternPosition.y );
1878
+ pattern.setAttribute( 'x', 0.0 );
1879
+ pattern.setAttribute( 'y', 0.0 );
1602
1880
  pattern.setAttribute( 'width', this._patternSize.x )
1603
1881
  pattern.setAttribute( 'height', this._patternSize.y );
1604
1882
  pattern.setAttribute( 'patternUnits', 'userSpaceOnUse' );
@@ -1638,9 +1916,9 @@ class GraphEditor {
1638
1916
  if( !this._background )
1639
1917
  return;
1640
1918
 
1641
- const patternSize = this._patternSize.mul( this._scale );
1642
- const circlePatternSize = this._circlePatternSize * this._scale;
1643
- const patternPosition = this._patternPosition.mul( this._scale );
1919
+ const patternSize = this._patternSize.mul( this.currentGraph.scale );
1920
+ const circlePatternSize = this._circlePatternSize * this.currentGraph.scale;
1921
+ const patternPosition = this.currentGraph.translation.mul( this.currentGraph.scale );
1644
1922
 
1645
1923
  let pattern = this._background.querySelector( 'pattern' );
1646
1924
  pattern.setAttribute( 'x', patternPosition.x );
@@ -1658,24 +1936,34 @@ class GraphEditor {
1658
1936
  const w = this._domNodes.offsetWidth * 0.5;
1659
1937
  const h = this._domNodes.offsetHeight * 0.5;
1660
1938
 
1661
- const dw = w - w * this._scale;
1662
- const dh = h - h * this._scale;
1939
+ const dw = w - w * this.currentGraph.scale;
1940
+ const dh = h - h * this.currentGraph.scale;
1663
1941
 
1664
1942
  this._domNodes.style.transform = `
1665
1943
  translate(` + ( patternPosition.x - dw ) + `px, ` + ( patternPosition.y - dh ) + `px)
1666
- scale(` + this._scale + `)
1944
+ scale(` + this.currentGraph.scale + `)
1667
1945
  `;
1668
1946
  this._domLinks.style.transform = this._domNodes.style.transform;
1947
+
1948
+ // Hide nodes outside the viewport
1949
+
1950
+ const nodesOutsideViewport = this._getNonVisibleNodes();
1951
+
1952
+ for( let node of nodesOutsideViewport )
1953
+ {
1954
+ let dom = this._getNodeDOMElement( node.id );
1955
+ dom.classList.toggle( 'hiddenOpacity', true );
1956
+ }
1669
1957
  }
1670
1958
 
1671
1959
  _getPatternPosition( renderPosition ) {
1672
1960
 
1673
- return renderPosition.div( this._scale ).sub( this._patternPosition );
1961
+ return renderPosition.div( this.currentGraph.scale ).sub( this.currentGraph.translation );
1674
1962
  }
1675
1963
 
1676
1964
  _getRenderPosition( patternPosition ) {
1677
1965
 
1678
- return patternPosition.add( this._patternPosition ).mul( this._scale );
1966
+ return patternPosition.add( this.currentGraph.translation ).mul( this.currentGraph.scale );
1679
1967
  }
1680
1968
 
1681
1969
  _onLink( e ) {
@@ -1738,11 +2026,6 @@ class GraphEditor {
1738
2026
  this._deleteLinks( src_nodeId, linkData.io );
1739
2027
  }
1740
2028
 
1741
- // Mark as active
1742
-
1743
- linkData.io.dataset[ 'active' ] = true;
1744
- e.target.parentElement.dataset[ 'active' ] = true;
1745
-
1746
2029
  // Store the end io..
1747
2030
 
1748
2031
  var srcDom = linkData.io;
@@ -1763,9 +2046,9 @@ class GraphEditor {
1763
2046
 
1764
2047
  const pathId = ( srcIsInput ? dst_nodeId : src_nodeId ) + '@' + ( srcIsInput ? src_nodeId : dst_nodeId );
1765
2048
 
1766
- if( !this.graph.links[ pathId ] ) this.graph.links[ pathId ] = [];
2049
+ if( !this.currentGraph.links[ pathId ] ) this.currentGraph.links[ pathId ] = [];
1767
2050
 
1768
- this.graph.links[ pathId ].push( {
2051
+ this.currentGraph.links[ pathId ].push( {
1769
2052
  path: path,
1770
2053
  inputNode: srcIsInput ? src_nodeId : dst_nodeId,
1771
2054
  inputIdx: srcIsInput ? src_ioIndex : dst_ioIndex,
@@ -1777,6 +2060,11 @@ class GraphEditor {
1777
2060
 
1778
2061
  path.dataset[ 'id' ] = pathId;
1779
2062
 
2063
+ // Mark as active links...
2064
+
2065
+ linkData.io.dataset[ 'active' ] = true;
2066
+ e.target.parentElement.dataset[ 'active' ] = true;
2067
+
1780
2068
  // Successful link..
1781
2069
  return true;
1782
2070
  }
@@ -1814,6 +2102,7 @@ class GraphEditor {
1814
2102
 
1815
2103
  let startPos = new LX.vec2( startRect.x - offsetX, startRect.y - offsetY );
1816
2104
  let endPos = null;
2105
+ let endioEl = null;
1817
2106
 
1818
2107
  if( e )
1819
2108
  {
@@ -1822,7 +2111,6 @@ class GraphEditor {
1822
2111
  // Add node position, since I can't get the correct position directly from the event..
1823
2112
  if( e.target.hasClass( [ 'lexgraphnode', 'lexgraphgroup' ] ) )
1824
2113
  {
1825
- console.log( this._getNodePosition( e.target ) );
1826
2114
  endPos.add( this._getNodePosition( e.target ), endPos );
1827
2115
  endPos.add( new LX.vec2( 3, 3 ), endPos );
1828
2116
  }
@@ -1840,7 +2128,8 @@ class GraphEditor {
1840
2128
  }
1841
2129
  else
1842
2130
  {
1843
- const ioRect = endIO.querySelector( '.io__type' ).getBoundingClientRect();
2131
+ endioEl = endIO.querySelector( '.io__type' );
2132
+ const ioRect = endioEl.getBoundingClientRect();
1844
2133
  endPos = new LX.vec2( ioRect.x - offsetX, ioRect.y - offsetY );
1845
2134
  }
1846
2135
 
@@ -1851,7 +2140,11 @@ class GraphEditor {
1851
2140
  startPos = tmp;
1852
2141
  }
1853
2142
 
1854
- const color = getComputedStyle( ioEl ).backgroundColor;
2143
+ let color = getComputedStyle( ioEl ).backgroundColor;
2144
+
2145
+ if( type == GraphEditor.NODE_IO_OUTPUT && endioEl )
2146
+ color = getComputedStyle( endioEl ).backgroundColor;
2147
+
1855
2148
  this._createLinkPath( path, startPos, endPos, color, !!e );
1856
2149
 
1857
2150
  return path;
@@ -1907,11 +2200,14 @@ class GraphEditor {
1907
2200
  io1.links = [ ];
1908
2201
  io1.links[ link.outputIdx ] = io1.links[ link.outputIdx ] ?? [ ];
1909
2202
  io1.links[ link.outputIdx ].push( link.outputNode );
2203
+
2204
+ io0.dataset[ 'active' ] = true;
2205
+ io1.dataset[ 'active' ] = true;
1910
2206
  }
1911
2207
 
1912
2208
  _createLinkPath( path, startPos, endPos, color, exactEnd ) {
1913
2209
 
1914
- const dist = 6 * this._scale;
2210
+ const dist = 6 * this.currentGraph.scale;
1915
2211
  startPos.add( new LX.vec2( dist, dist ), startPos );
1916
2212
 
1917
2213
  if( !exactEnd )
@@ -2062,16 +2358,44 @@ class GraphEditor {
2062
2358
  "/>`;
2063
2359
  }
2064
2360
 
2065
- // TODO: Return the ones in the viewport
2066
- _getVisibleNodes() {
2361
+ _getNonVisibleNodes() {
2362
+
2363
+ const nonVisibleNodes = [ ];
2067
2364
 
2068
- if( !this.graph )
2365
+ if( !this.currentGraph )
2069
2366
  {
2070
2367
  console.warn( "No graph set" );
2071
2368
  return [];
2072
2369
  }
2073
2370
 
2074
- return this.graph.nodes;
2371
+ const graph_bb = new BoundingBox( new LX.vec2( 0, 0 ), new LX.vec2( this.root.offsetWidth, this.root.offsetHeight ) );
2372
+
2373
+ for( let node of this.currentGraph.nodes )
2374
+ {
2375
+ let pos = this._getRenderPosition( node.position );
2376
+
2377
+ let dom = this._getNodeDOMElement( node.id );
2378
+
2379
+ if( !dom )
2380
+ continue;
2381
+
2382
+ const node_bb = new BoundingBox( pos, node.size.mul( this.currentGraph.scale ) );
2383
+
2384
+ if( graph_bb.inside( node_bb, false ) )
2385
+ {
2386
+ // Show if node in viewport..
2387
+ dom.classList.toggle( 'hiddenOpacity', false );
2388
+
2389
+ // And hide content if scale is very small..
2390
+ dom.childNodes[ 1 ].classList.toggle( 'hiddenOpacity', this.currentGraph.scale < 0.5 );
2391
+
2392
+ continue;
2393
+ }
2394
+
2395
+ nonVisibleNodes.push( node );
2396
+ }
2397
+
2398
+ return nonVisibleNodes;
2075
2399
  }
2076
2400
 
2077
2401
  _selectNodesInBox( lt, rb, remove ) {
@@ -2145,11 +2469,8 @@ class GraphEditor {
2145
2469
 
2146
2470
  for( let nodeId of nodeIds )
2147
2471
  {
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 ) );
2472
+ const node = this.nodes[ nodeId ].data;
2473
+ const node_bb = new BoundingBox( node.position, node.size );
2153
2474
 
2154
2475
  if( group_bb )
2155
2476
  {
@@ -2180,11 +2501,13 @@ class GraphEditor {
2180
2501
  * @returns JSON data from the serialized graph
2181
2502
  */
2182
2503
 
2183
- _createGroup() {
2504
+ _createGroup( bb ) {
2184
2505
 
2185
- const group_bb = this._getBoundingFromNodes( this.selectedNodes );
2506
+ const group_bb = bb ?? this._getBoundingFromNodes( this.selectedNodes );
2507
+ const group_id = bb ? bb.id : "group-" + LX.UTILS.uidGenerator();
2186
2508
 
2187
2509
  let groupDOM = document.createElement( 'div' );
2510
+ groupDOM.id = group_id;
2188
2511
  groupDOM.classList.add( 'lexgraphgroup' );
2189
2512
  groupDOM.style.left = group_bb.origin.x + "px";
2190
2513
  groupDOM.style.top = group_bb.origin.y + "px";
@@ -2196,6 +2519,8 @@ class GraphEditor {
2196
2519
 
2197
2520
  groupResizer.addEventListener( 'mousedown', inner_mousedown );
2198
2521
 
2522
+ this.groups[ group_id ] = groupDOM;
2523
+
2199
2524
  var that = this;
2200
2525
  var lastPos = [0,0];
2201
2526
 
@@ -2215,7 +2540,7 @@ class GraphEditor {
2215
2540
  function inner_mousemove( e )
2216
2541
  {
2217
2542
  let dt = new LX.vec2( lastPos[0] - e.x, lastPos[1] - e.y );
2218
- dt.div( that._scale, dt);
2543
+ dt.div( that.currentGraph.scale, dt);
2219
2544
 
2220
2545
  groupDOM.style.width = ( parseFloat( groupDOM.style.width ) - dt.x ) + "px";
2221
2546
  groupDOM.style.height = ( parseFloat( groupDOM.style.height ) - dt.y ) + "px";
@@ -2273,6 +2598,19 @@ class GraphEditor {
2273
2598
  groupTitle.focus();
2274
2599
  } );
2275
2600
 
2601
+ groupDOM.addEventListener( 'contextmenu', e => {
2602
+
2603
+ e.preventDefault();
2604
+ e.stopPropagation();
2605
+ e.stopImmediatePropagation();
2606
+
2607
+ LX.addContextMenu(null, e, m => {
2608
+ m.add( "Delete", () => {
2609
+ this._deleteGroup( group_id );
2610
+ } );
2611
+ });
2612
+ } );
2613
+
2276
2614
  groupDOM.appendChild( groupResizer );
2277
2615
  groupDOM.appendChild( groupTitle );
2278
2616
 
@@ -2286,6 +2624,8 @@ class GraphEditor {
2286
2624
  } );
2287
2625
 
2288
2626
  GraphEditor.LAST_GROUP_ID++;
2627
+
2628
+ return groupDOM;
2289
2629
  }
2290
2630
 
2291
2631
  _addUndoStep( deleteRedo = true ) {
@@ -2370,78 +2710,104 @@ class GraphEditor {
2370
2710
  }
2371
2711
  }
2372
2712
 
2373
- _toggleSideBar() {
2713
+ _toggleSideBar( force ) {
2374
2714
 
2375
- this._sidebar.classList.toggle( 'hidden' );
2715
+ this._sidebarActive = force ?? !this._sidebarActive;
2716
+ this._sidebarDom.classList.toggle( 'hidden', !this._sidebarActive );
2717
+ this._graphContainer.style.width = this._sidebarActive ? "calc( 100% - 64px )" : "100%";
2376
2718
  }
2377
- }
2378
2719
 
2379
- LX.GraphEditor = GraphEditor;
2720
+ _onSidebarCreate( e ) {
2721
+
2722
+ LX.addContextMenu(null, e, m => {
2723
+ m.add( "Graph", () => this.addGraph() );
2724
+ m.add( "Function", () => this.addGraphFunction() );
2725
+ });
2726
+ }
2380
2727
 
2728
+ _showRenameGraphDialog() {
2381
2729
 
2382
- /**
2383
- * @class Graph
2384
- */
2730
+ const dialog = new LX.Dialog( this.currentGraph.constructor.name, p => {
2731
+ p.addText( "Name", this.currentGraph.name, v => {
2732
+ this._updateGraphName( v );
2733
+ dialog.close();
2734
+ } );
2735
+ }, { modal: true, size: [ "350px", null ] } );
2736
+ }
2385
2737
 
2386
- class Graph {
2738
+ _updateGraphName( name ) {
2387
2739
 
2388
- /**
2389
- * @param {*} options
2390
- *
2391
- */
2740
+ const newNameKey = name.replace( /\s/g, '' ).replaceAll( '.', '' );
2392
2741
 
2393
- constructor( name, options = {} ) {
2742
+ // Change graph name button
2743
+ const nameDom = LX.root.querySelector( '.graph-title button' );
2744
+ console.assert( nameDom );
2745
+ nameDom.innerText = name;
2394
2746
 
2395
- this.name = name ?? "Unnamed Graph";
2747
+ // Change name in sidebar
2748
+ const graphNameKey = this.currentGraph.name.replace( /\s/g, '' ).replaceAll( '.', '' );
2749
+ const sidebarItem = this._sidebar.items.find( v => v.name === graphNameKey );
2750
+ if( sidebarItem )
2751
+ {
2752
+ sidebarItem.name = newNameKey;
2753
+ sidebarItem.domEl.id = newNameKey;
2754
+ sidebarItem.domEl.querySelector(".lexsidebarentrydesc").innerText = name;
2755
+ }
2396
2756
 
2397
- // Nodes
2757
+ // Change registered nodes function
2758
+ const oldType = 'function/' + this.currentGraph.name;
2759
+ const nodeClass = GraphEditor.NODE_TYPES[ oldType ];
2398
2760
 
2399
- this.nodes = [ ];
2761
+ if( nodeClass )
2762
+ {
2763
+ delete GraphEditor.NODE_TYPES[ oldType ];
2764
+
2765
+ nodeClass.title = name;
2766
+
2767
+ GraphEditor.registerCustomNode( "function/" + name, nodeClass );
2768
+ }
2400
2769
 
2401
- this.links = { };
2770
+ this.currentGraph.name = name;
2771
+ }
2402
2772
 
2403
- // const mainNode = GraphEditor.addNode( 'system/Main' );
2404
- // mainNode.position = new LX.vec2( 650, 400 );
2773
+ _addGlobalActions() {
2405
2774
 
2406
- // const addNode = GraphEditor.addNode( 'math/Add' );
2407
- // addNode.position = new LX.vec2( 425, 250 );
2775
+
2776
+ }
2777
+ }
2408
2778
 
2409
- // const floatNode = GraphEditor.addNode( 'inputs/Float' );
2410
- // floatNode.position = new LX.vec2( 200, 200 );
2779
+ LX.GraphEditor = GraphEditor;
2411
2780
 
2412
- // const stringNode = GraphEditor.addNode( 'inputs/String' );
2413
- // stringNode.position = new LX.vec2( 400, 50 );
2414
2781
 
2415
- // const setVarNode = GraphEditor.addNode( 'variables/SetVariable' );
2416
- // setVarNode.position = new LX.vec2( 650, 50 );
2782
+ /**
2783
+ * @class Graph
2784
+ */
2417
2785
 
2418
- // // const multNode = new GraphNode( GraphEditor.DEFAULT_NODES[ 'Multiply' ] );
2419
- // // multNode.position = new LX.vec2( 200, 400 );
2786
+ class Graph {
2420
2787
 
2421
- // const keydownNode = GraphEditor.addNode( 'events/KeyDown' );
2422
- // keydownNode.position = new LX.vec2( 600, 200 );
2788
+ /**
2789
+ * @param {*} options
2790
+ *
2791
+ */
2423
2792
 
2424
- // const orNode = GraphEditor.addNode( 'logic/Or' );
2425
- // orNode.position = new LX.vec2( 435, 435 );
2793
+ constructor( name, options = {} ) {
2426
2794
 
2427
- // const equalNode = GraphEditor.addNode( 'logic/Select' );
2428
- // equalNode.position = new LX.vec2( 135, 400 );
2795
+ this.name = name ?? "Unnamed Graph";
2796
+ this.type = 'Graph';
2429
2797
 
2430
- // this.nodes = [
2431
- // mainNode,
2432
- // addNode,
2433
- // floatNode,
2434
- // setVarNode,
2435
- // stringNode,
2436
- // keydownNode,
2437
- // orNode,
2438
- // equalNode,
2439
- // // multNode
2440
- // ];
2798
+ this.nodes = [ ];
2799
+ this.groups = [ ];
2800
+ this.links = { };
2801
+
2802
+ this.scale = 1.0;
2803
+ this.translation = new LX.vec2( 0, 0 );
2441
2804
  }
2442
2805
 
2443
2806
  configure( o ) {
2444
2807
 
2808
+ this.id = o.id;
2809
+ this.name = o.name;
2810
+
2445
2811
  this.nodes.length = 0;
2446
2812
 
2447
2813
  for( let node of o.nodes )
@@ -2453,13 +2819,12 @@ class Graph {
2453
2819
  newNode.color = node.color;
2454
2820
  newNode.position = new LX.vec2( node.position.x, node.position.y );
2455
2821
  newNode.type = node.type;
2456
- // newNode.inputs = node.inputs;
2457
- // newNode.outputs = node.outputs;
2458
- // newNode.properties = node.properties;
2822
+ newNode.properties = node.properties;
2459
2823
 
2460
2824
  this.nodes.push( newNode );
2461
2825
  }
2462
2826
 
2827
+ this.groups = o.groups;
2463
2828
  this.links = o.links;
2464
2829
 
2465
2830
  // editor options?
@@ -2468,20 +2833,111 @@ class Graph {
2468
2833
  }
2469
2834
 
2470
2835
  /**
2471
- * @method export
2836
+ * @method getNodeById
2837
+ */
2838
+
2839
+ getNodeById( id ) {
2840
+
2841
+ for( let node of this.nodes )
2842
+ {
2843
+ if( node.id == id ) return node;
2844
+ }
2845
+ }
2846
+
2847
+ /**
2848
+ * @method _runStep
2849
+ */
2850
+
2851
+ _runStep( mainId ) {
2852
+
2853
+ if( !mainId )
2854
+ return;
2855
+
2856
+ const nodes = this.nodes.reduce( ( ac, a ) => ( {...ac, [ a.id ] : a } ), {} );
2857
+
2858
+ // Not main graph..
2859
+ if( !nodes[ mainId ] )
2860
+ return;
2861
+
2862
+ const visitedNodes = { };
2863
+
2864
+ this._executionNodes = [ ];
2865
+
2866
+ // Reser variables each step?
2867
+ this.variables = { };
2868
+
2869
+ const addNode = ( id ) => {
2870
+
2871
+ if( visitedNodes[ id ] )
2872
+ return;
2873
+
2874
+ visitedNodes[ id ] = true;
2875
+
2876
+ for( let linkId in this.links )
2877
+ {
2878
+ const idx = linkId.indexOf( '@' + id );
2879
+
2880
+ if( idx < 0 )
2881
+ continue;
2882
+
2883
+ const preNodeId = linkId.substring( 0, idx );
2884
+
2885
+ this._executionNodes.push( preNodeId );
2886
+
2887
+ addNode( preNodeId );
2888
+ }
2889
+ };
2890
+
2891
+ // TODO: Search "no output" nodes and add to the executable list (same as main)..
2892
+ // ...
2893
+
2894
+ this._executionNodes.push( mainId );
2895
+
2896
+ addNode( mainId );
2897
+
2898
+ for( var i = this._executionNodes.length - 1; i >= 0; --i )
2899
+ {
2900
+ const node = nodes[ this._executionNodes[ i ] ];
2901
+
2902
+ if( node.onBeforeStep )
2903
+ node.onBeforeStep();
2904
+
2905
+ node.execute();
2906
+
2907
+ if( node.onBeforeStep )
2908
+ node.onAfterStep();
2909
+ }
2910
+ }
2911
+
2912
+ /**
2913
+ * @method serialize
2472
2914
  * @param {Boolean} prettify
2473
2915
  * @returns JSON data from the serialized graph
2474
2916
  */
2475
2917
 
2476
- export( prettify = true ) {
2918
+ serialize( prettify = true ) {
2477
2919
 
2478
2920
  var o = { };
2479
- o.nodes = [ ];
2480
- o.links = { };
2921
+
2922
+ o.id = this.id;
2923
+ o.name = this.name;
2924
+ o.type = this.type;
2925
+
2926
+ o.nodes = [ ];
2927
+ o.groups = [ ];
2928
+ o.functions = [ ];
2929
+ o.links = { };
2481
2930
 
2482
2931
  for( let node of this.nodes )
2483
2932
  {
2484
2933
  o.nodes.push( node.serialize() );
2934
+
2935
+ const fnOrigin = this.editor.graphs[ node.gid ];
2936
+
2937
+ if( fnOrigin )
2938
+ {
2939
+ o.functions.push( JSON.parse( fnOrigin.serialize() ) );
2940
+ }
2485
2941
  }
2486
2942
 
2487
2943
  for( let linkId in this.links )
@@ -2491,6 +2947,15 @@ class Graph {
2491
2947
  o.links[ linkId ] = ioLinks;
2492
2948
  }
2493
2949
 
2950
+ for( let group of this.groups )
2951
+ {
2952
+ const groupDom = this.editor.groups[ group.id ];
2953
+ const group_bb = this.editor._getBoundingFromGroup( groupDom );
2954
+ group_bb.id = group.id;
2955
+ group_bb.name = group.name
2956
+ o.groups.push( group_bb );
2957
+ }
2958
+
2494
2959
  // editor options?
2495
2960
 
2496
2961
  // zoom/translation ??
@@ -2505,10 +2970,19 @@ class Graph {
2505
2970
  console.error( `Can't export GraphNode [${ this.title }] of type [${ this.type }].` );
2506
2971
  }
2507
2972
 
2508
- LX.downloadFile( this.name + ".json", o );
2509
-
2510
2973
  return o;
2511
2974
  }
2975
+
2976
+ /**
2977
+ * @method export
2978
+ */
2979
+
2980
+ export() {
2981
+
2982
+ const o = this.serialize();
2983
+
2984
+ LX.downloadFile( this.name + ".json", o );
2985
+ }
2512
2986
  }
2513
2987
 
2514
2988
  LX.Graph = Graph;
@@ -2562,16 +3036,18 @@ class GraphNode {
2562
3036
  if( !this.inputs || !this.inputs.length || !this.inputs[ index ] )
2563
3037
  return;
2564
3038
 
3039
+ const graph = this.editor.graphs[ this.graphID ];
3040
+
2565
3041
  // Get data from link
2566
3042
 
2567
- for( let linkId in this.editor.graph.links )
3043
+ for( let linkId in graph.links )
2568
3044
  {
2569
3045
  const idx = linkId.indexOf( '@' + this.id );
2570
3046
 
2571
3047
  if( idx < 0 )
2572
3048
  continue;
2573
3049
 
2574
- const nodeLinks = this.editor.graph.links[ linkId ];
3050
+ const nodeLinks = graph.links[ linkId ];
2575
3051
 
2576
3052
  for ( var link of nodeLinks )
2577
3053
  {
@@ -2589,23 +3065,27 @@ class GraphNode {
2589
3065
  if( !this.outputs || !this.outputs.length || !this.outputs[ index ] )
2590
3066
  return;
2591
3067
 
3068
+ const graph = this.editor.graphs[ this.graphID ];
3069
+
2592
3070
  // Set data in link
2593
3071
 
2594
- for( let linkId in this.editor.graph.links )
3072
+ for( let linkId in graph.links )
2595
3073
  {
2596
3074
  const idx = linkId.indexOf( this.id + '@' );
2597
3075
 
2598
3076
  if( idx < 0 )
2599
3077
  continue;
2600
3078
 
2601
- const nodeLinks = this.editor.graph.links[ linkId ];
3079
+ const nodeLinks = graph.links[ linkId ];
2602
3080
 
2603
3081
  for ( var link of nodeLinks )
2604
3082
  {
2605
3083
  if( link.outputIdx != index )
2606
3084
  continue;
2607
3085
 
2608
- if( data != undefined && link.inputType != link.outputType && link.inputType != "any" && link.outputType != "any" )
3086
+ let innerData = data;
3087
+
3088
+ if( innerData != undefined && link.inputType != link.outputType && link.inputType != "any" && link.outputType != "any" )
2609
3089
  {
2610
3090
  // In case of supported casting, use function to cast..
2611
3091
 
@@ -2613,19 +3093,14 @@ class GraphNode {
2613
3093
 
2614
3094
  // Use function if it's possible to cast!
2615
3095
 
2616
- data = fn ? fn( LX.deepCopy( data ) ) : null;
3096
+ innerData = fn ? fn( LX.deepCopy( innerData ) ) : null;
2617
3097
  }
2618
3098
 
2619
- link.data = data;
3099
+ link.data = innerData;
2620
3100
  }
2621
3101
  }
2622
3102
  }
2623
3103
 
2624
- getOutput( index ) {
2625
-
2626
- return this.outputs[ index ].value;
2627
- }
2628
-
2629
3104
  serialize() {
2630
3105
 
2631
3106
  var o = { };
@@ -2645,6 +3120,45 @@ class GraphNode {
2645
3120
 
2646
3121
  LX.GraphNode = GraphNode;
2647
3122
 
3123
+ /**
3124
+ * @class GraphFunction
3125
+ */
3126
+
3127
+ class GraphFunction extends Graph {
3128
+
3129
+ constructor( name, options = { } ) {
3130
+
3131
+ super();
3132
+
3133
+ this.name = name ?? ( "GraphFunction" + GraphEditor.LAST_FUNCTION_ID );
3134
+ this.type = 'GraphFunction';
3135
+
3136
+ GraphEditor.LAST_FUNCTION_ID++
3137
+
3138
+ const nodeInput = GraphEditor.addNode( "function/Input" );
3139
+ nodeInput.position = new LX.vec2( 150, 250 );
3140
+
3141
+ const nodeOutput = GraphEditor.addNode( "function/Output" );
3142
+ nodeOutput.position = new LX.vec2( 650, 350 );
3143
+
3144
+ this.nodes.push( nodeInput, nodeOutput );
3145
+ }
3146
+
3147
+ getOutputData( inputValue ) {
3148
+
3149
+ const inputNode = this.nodes[ 0 ];
3150
+ inputNode.setOutput( 0, inputValue );
3151
+
3152
+ const outputNode = this.nodes[ 1 ];
3153
+
3154
+ this._runStep( outputNode.id );
3155
+
3156
+ return outputNode.getInput( 0 );
3157
+ }
3158
+ }
3159
+
3160
+ LX.GraphFunction = GraphFunction;
3161
+
2648
3162
  /*
2649
3163
  ************ PREDEFINED NODES ************
2650
3164
 
@@ -2656,6 +3170,54 @@ LX.GraphNode = GraphNode;
2656
3170
  - onExecute: Callback for node execution
2657
3171
  */
2658
3172
 
3173
+ /*
3174
+ Function nodes
3175
+ */
3176
+
3177
+ class NodeFuncInput extends GraphNode
3178
+ {
3179
+ onCreate() {
3180
+ this.addOutput( null, "float" );
3181
+ this.addProperty( "Outputs", "array", [ "float" ], [ 'float', 'int', 'bool', 'vec2', 'vec3', 'vec4', 'mat44' ] );
3182
+ }
3183
+
3184
+ onExecute() {
3185
+ // var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
3186
+ // var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
3187
+ // this.setOutput( 0, a + b );
3188
+ }
3189
+
3190
+ setOutputs( v ) {
3191
+
3192
+ this.outputs.length = 0;
3193
+
3194
+ for( var i of v )
3195
+ {
3196
+ this.outputs.push( { name: null, type: i } );
3197
+ }
3198
+ }
3199
+ }
3200
+
3201
+ NodeFuncInput.blockDelete = true;
3202
+ NodeFuncInput.blockAdd = true;
3203
+ GraphEditor.registerCustomNode( "function/Input", NodeFuncInput );
3204
+
3205
+ class NodeFuncOutput extends GraphNode
3206
+ {
3207
+ onCreate() {
3208
+ this.addInput( null, "any" );
3209
+ }
3210
+
3211
+ onExecute() {
3212
+ // var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
3213
+ // var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
3214
+ // this.setOutput( 0, a + b );
3215
+ }
3216
+ }
3217
+
3218
+ NodeFuncOutput.blockDelete = true;
3219
+ NodeFuncOutput.blockAdd = true;
3220
+ GraphEditor.registerCustomNode( "function/Output", NodeFuncOutput );
2659
3221
 
2660
3222
  /*
2661
3223
  Math nodes
@@ -3047,7 +3609,7 @@ class NodeSetVariable extends GraphNode
3047
3609
  var varValue = this.getInput( 1 );
3048
3610
  if( varValue == undefined )
3049
3611
  return;
3050
- this.graph.setVariable( varName, varValue );
3612
+ this.editor.setVariable( varName, varValue );
3051
3613
  this.setOutput( 0, varValue );
3052
3614
  }
3053
3615
  }
@@ -3112,6 +3674,7 @@ class NodeMain extends GraphNode
3112
3674
  };
3113
3675
  }
3114
3676
 
3677
+ NodeMain.blockDelete = true;
3115
3678
  GraphEditor.registerCustomNode( "system/Main", NodeMain );
3116
3679
 
3117
3680
  export { GraphEditor, Graph, GraphNode };